logdetective 0.9.0__tar.gz → 0.10.0__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.
- {logdetective-0.9.0 → logdetective-0.10.0}/PKG-INFO +3 -2
- {logdetective-0.9.0 → logdetective-0.10.0}/README.md +1 -1
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/constants.py +4 -0
- {logdetective-0.9.0/logdetective/server → logdetective-0.10.0/logdetective}/remote_log.py +3 -43
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/compressors.py +49 -4
- logdetective-0.9.0/logdetective/server/utils.py → logdetective-0.10.0/logdetective/server/config.py +12 -13
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/database/models/merge_request_jobs.py +79 -7
- logdetective-0.10.0/logdetective/server/emoji.py +104 -0
- logdetective-0.10.0/logdetective/server/gitlab.py +413 -0
- logdetective-0.10.0/logdetective/server/llm.py +284 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/metric.py +9 -8
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/models.py +78 -6
- logdetective-0.10.0/logdetective/server/server.py +514 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/utils.py +1 -1
- {logdetective-0.9.0 → logdetective-0.10.0}/pyproject.toml +8 -2
- logdetective-0.9.0/logdetective/server/server.py +0 -981
- {logdetective-0.9.0 → logdetective-0.10.0}/LICENSE +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/__init__.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/drain3.ini +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/extractors.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/logdetective.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/models.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/prompts.yml +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/__init__.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/database/__init__.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/database/base.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/database/models/__init__.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/database/models/metrics.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/plot.py +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/templates/gitlab_full_comment.md.j2 +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/templates/gitlab_short_comment.md.j2 +0 -0
- {logdetective-0.9.0 → logdetective-0.10.0}/logdetective.1.asciidoc +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Log using LLM AI to search for build/test failures and provide ideas for fixing these.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Jiri Podivin
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Debuggers
|
|
|
21
21
|
Provides-Extra: server
|
|
22
22
|
Provides-Extra: server-testing
|
|
23
23
|
Requires-Dist: aiohttp (>=3.7.4)
|
|
24
|
+
Requires-Dist: aiolimiter (>=1.0.0,<2.0.0) ; extra == "server"
|
|
24
25
|
Requires-Dist: alembic (>=1.13.3,<2.0.0) ; extra == "server" or extra == "server-testing"
|
|
25
26
|
Requires-Dist: backoff (==2.2.1) ; extra == "server" or extra == "server-testing"
|
|
26
27
|
Requires-Dist: drain3 (>=0.9.11,<0.10.0)
|
|
@@ -60,7 +61,7 @@ Installation
|
|
|
60
61
|
|
|
61
62
|
**From Pypi repository**
|
|
62
63
|
|
|
63
|
-
The logdetective project is published on the
|
|
64
|
+
The logdetective project is published on the [Pypi repository](https://pypi.org/project/logdetective/). The `pip` tool can be used for installation.
|
|
64
65
|
|
|
65
66
|
First, ensure that the necessary dependencies for the `llama-cpp-python` project are installed. For Fedora, install `gcc-c++`:
|
|
66
67
|
|
|
@@ -18,7 +18,7 @@ Installation
|
|
|
18
18
|
|
|
19
19
|
**From Pypi repository**
|
|
20
20
|
|
|
21
|
-
The logdetective project is published on the
|
|
21
|
+
The logdetective project is published on the [Pypi repository](https://pypi.org/project/logdetective/). The `pip` tool can be used for installation.
|
|
22
22
|
|
|
23
23
|
First, ensure that the necessary dependencies for the `llama-cpp-python` project are installed. For Fedora, install `gcc-c++`:
|
|
24
24
|
|
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
import io
|
|
2
1
|
import logging
|
|
3
|
-
from typing import Union
|
|
4
2
|
from urllib.parse import urlparse
|
|
5
3
|
|
|
6
4
|
import aiohttp
|
|
7
|
-
|
|
8
|
-
from logdetective.server.compressors import TextCompressor
|
|
9
|
-
|
|
5
|
+
from fastapi import HTTPException
|
|
10
6
|
|
|
11
7
|
LOG = logging.getLogger("logdetective")
|
|
12
8
|
|
|
13
9
|
|
|
14
10
|
class RemoteLog:
|
|
15
11
|
"""
|
|
16
|
-
Handles retrieval
|
|
12
|
+
Handles retrieval of remote log files.
|
|
17
13
|
"""
|
|
18
14
|
|
|
19
|
-
LOG_FILE_NAME = "log.txt"
|
|
20
|
-
COMPRESSOR = TextCompressor()
|
|
21
|
-
|
|
22
15
|
def __init__(self, url: str, http_session: aiohttp.ClientSession):
|
|
23
16
|
"""
|
|
24
17
|
Initialize with a remote log URL and HTTP session.
|
|
@@ -40,39 +33,6 @@ class RemoteLog:
|
|
|
40
33
|
"""Content of the url."""
|
|
41
34
|
return await self.get_url_content()
|
|
42
35
|
|
|
43
|
-
@classmethod
|
|
44
|
-
def zip_text(cls, text: str) -> bytes:
|
|
45
|
-
"""
|
|
46
|
-
Compress the given text.
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
bytes: Compressed text
|
|
50
|
-
"""
|
|
51
|
-
return cls.COMPRESSOR.zip({cls.LOG_FILE_NAME: text})
|
|
52
|
-
|
|
53
|
-
async def zip_content(self) -> bytes:
|
|
54
|
-
"""
|
|
55
|
-
Compress the content of the remote log.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
bytes: Compressed log content
|
|
59
|
-
"""
|
|
60
|
-
content_text = await self.content
|
|
61
|
-
return self.zip_text(content_text)
|
|
62
|
-
|
|
63
|
-
@classmethod
|
|
64
|
-
def unzip(cls, zip_data: Union[bytes, io.BytesIO]) -> str:
|
|
65
|
-
"""
|
|
66
|
-
Uncompress the zipped content of the remote log.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
zip_data: Compressed data as bytes or BytesIO
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
str: The decompressed log content
|
|
73
|
-
"""
|
|
74
|
-
return cls.COMPRESSOR.unzip(zip_data)[cls.LOG_FILE_NAME]
|
|
75
|
-
|
|
76
36
|
def validate_url(self) -> bool:
|
|
77
37
|
"""Validate incoming URL to be at least somewhat sensible for log files
|
|
78
38
|
Only http and https protocols permitted. No result, params or query fields allowed.
|
|
@@ -104,6 +64,6 @@ class RemoteLog:
|
|
|
104
64
|
try:
|
|
105
65
|
return await self.get_url_content()
|
|
106
66
|
except RuntimeError as ex:
|
|
107
|
-
raise
|
|
67
|
+
raise HTTPException(
|
|
108
68
|
status_code=400, detail=f"We couldn't obtain the logs: {ex}"
|
|
109
69
|
) from ex
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import io
|
|
2
|
-
import logging
|
|
3
2
|
import zipfile
|
|
4
3
|
|
|
5
4
|
from typing import Union, Dict
|
|
5
|
+
from logdetective.remote_log import RemoteLog
|
|
6
6
|
from logdetective.server.models import (
|
|
7
7
|
StagedResponse,
|
|
8
8
|
Response,
|
|
@@ -11,9 +11,6 @@ from logdetective.server.models import (
|
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
LOG = logging.getLogger("logdetective")
|
|
15
|
-
|
|
16
|
-
|
|
17
14
|
class TextCompressor:
|
|
18
15
|
"""
|
|
19
16
|
Encapsulates one or more texts in one or more files with the specified names
|
|
@@ -63,6 +60,54 @@ class TextCompressor:
|
|
|
63
60
|
return content
|
|
64
61
|
|
|
65
62
|
|
|
63
|
+
class RemoteLogCompressor:
|
|
64
|
+
"""
|
|
65
|
+
Handles compression of remote log files.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
LOG_FILE_NAME = "log.txt"
|
|
69
|
+
COMPRESSOR = TextCompressor()
|
|
70
|
+
|
|
71
|
+
def __init__(self, remote_log: RemoteLog):
|
|
72
|
+
"""
|
|
73
|
+
Initialize with a RemoteLog object.
|
|
74
|
+
"""
|
|
75
|
+
self._remote_log = remote_log
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def zip_text(cls, text: str) -> bytes:
|
|
79
|
+
"""
|
|
80
|
+
Compress the given text.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
bytes: Compressed text
|
|
84
|
+
"""
|
|
85
|
+
return cls.COMPRESSOR.zip({cls.LOG_FILE_NAME: text})
|
|
86
|
+
|
|
87
|
+
async def zip_content(self) -> bytes:
|
|
88
|
+
"""
|
|
89
|
+
Compress the content of the remote log.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
bytes: Compressed log content
|
|
93
|
+
"""
|
|
94
|
+
content_text = await self._remote_log.content
|
|
95
|
+
return self.zip_text(content_text)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def unzip(cls, zip_data: Union[bytes, io.BytesIO]) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Uncompress the zipped content of the remote log.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
zip_data: Compressed data as bytes or BytesIO
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: The decompressed log content
|
|
107
|
+
"""
|
|
108
|
+
return cls.COMPRESSOR.unzip(zip_data)[cls.LOG_FILE_NAME]
|
|
109
|
+
|
|
110
|
+
|
|
66
111
|
class LLMResponseCompressor:
|
|
67
112
|
"""
|
|
68
113
|
Handles compression and decompression of LLM responses.
|
logdetective-0.9.0/logdetective/server/utils.py → logdetective-0.10.0/logdetective/server/config.py
RENAMED
|
@@ -1,18 +1,8 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import logging
|
|
2
3
|
import yaml
|
|
3
|
-
from logdetective.
|
|
4
|
-
from logdetective.server.models import Config
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def format_analyzed_snippets(snippets: list[AnalyzedSnippet]) -> str:
|
|
8
|
-
"""Format snippets for submission into staged prompt."""
|
|
9
|
-
summary = f"\n{SNIPPET_DELIMITER}\n".join(
|
|
10
|
-
[
|
|
11
|
-
f"[{e.text}] at line [{e.line_number}]: [{e.explanation.text}]"
|
|
12
|
-
for e in snippets
|
|
13
|
-
]
|
|
14
|
-
)
|
|
15
|
-
return summary
|
|
4
|
+
from logdetective.utils import load_prompts
|
|
5
|
+
from logdetective.server.models import Config
|
|
16
6
|
|
|
17
7
|
|
|
18
8
|
def load_server_config(path: str | None) -> Config:
|
|
@@ -57,3 +47,12 @@ def get_log(config: Config):
|
|
|
57
47
|
|
|
58
48
|
log.initialized = True
|
|
59
49
|
return log
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
SERVER_CONFIG_PATH = os.environ.get("LOGDETECTIVE_SERVER_CONF", None)
|
|
53
|
+
SERVER_PROMPT_PATH = os.environ.get("LOGDETECTIVE_PROMPTS", None)
|
|
54
|
+
|
|
55
|
+
SERVER_CONFIG = load_server_config(SERVER_CONFIG_PATH)
|
|
56
|
+
PROMPT_CONFIG = load_prompts(SERVER_PROMPT_PATH)
|
|
57
|
+
|
|
58
|
+
LOG = get_log(SERVER_CONFIG)
|
{logdetective-0.9.0 → logdetective-0.10.0}/logdetective/server/database/models/merge_request_jobs.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import datetime
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, List
|
|
4
4
|
|
|
5
5
|
import backoff
|
|
6
6
|
|
|
@@ -131,6 +131,24 @@ class GitlabMergeRequestJobs(Base):
|
|
|
131
131
|
)
|
|
132
132
|
return mr
|
|
133
133
|
|
|
134
|
+
@classmethod
|
|
135
|
+
def get_by_mr_iid(
|
|
136
|
+
cls, forge: Forge, project_id: int, mr_iid
|
|
137
|
+
) -> Optional["GitlabMergeRequestJobs"]:
|
|
138
|
+
"""Get all the mr jobs saved for the specified mr iid and project id."""
|
|
139
|
+
with transaction(commit=False) as session:
|
|
140
|
+
comments = (
|
|
141
|
+
session.query(cls)
|
|
142
|
+
.filter(
|
|
143
|
+
GitlabMergeRequestJobs.forge == forge,
|
|
144
|
+
GitlabMergeRequestJobs.project_id == project_id,
|
|
145
|
+
GitlabMergeRequestJobs.mr_iid == mr_iid,
|
|
146
|
+
)
|
|
147
|
+
.all()
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return comments
|
|
151
|
+
|
|
134
152
|
@classmethod
|
|
135
153
|
def get_or_create(
|
|
136
154
|
cls,
|
|
@@ -170,12 +188,7 @@ class Comments(Base):
|
|
|
170
188
|
index=True,
|
|
171
189
|
comment="The associated merge request job (db) id",
|
|
172
190
|
)
|
|
173
|
-
forge = Column(
|
|
174
|
-
Enum(Forge),
|
|
175
|
-
nullable=False,
|
|
176
|
-
index=True,
|
|
177
|
-
comment="The forge name"
|
|
178
|
-
)
|
|
191
|
+
forge = Column(Enum(Forge), nullable=False, index=True, comment="The forge name")
|
|
179
192
|
comment_id = Column(
|
|
180
193
|
String(50), # e.g. 'd5a3ff139356ce33e37e73add446f16869741b50'
|
|
181
194
|
nullable=False,
|
|
@@ -362,6 +375,36 @@ class Comments(Base):
|
|
|
362
375
|
comment = GitlabMergeRequestJobs.get_by_id(id_)
|
|
363
376
|
return comment
|
|
364
377
|
|
|
378
|
+
@classmethod
|
|
379
|
+
def get_since(cls, time: datetime.datetime) -> Optional["Comments"]:
|
|
380
|
+
"""Get all the comments created after the given time."""
|
|
381
|
+
with transaction(commit=False) as session:
|
|
382
|
+
comments = (
|
|
383
|
+
session.query(cls)
|
|
384
|
+
.filter(
|
|
385
|
+
Comments.created_at > time,
|
|
386
|
+
)
|
|
387
|
+
.all()
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return comments
|
|
391
|
+
|
|
392
|
+
@classmethod
|
|
393
|
+
def get_by_mr_job(
|
|
394
|
+
cls, merge_request_job: GitlabMergeRequestJobs
|
|
395
|
+
) -> Optional["Comments"]:
|
|
396
|
+
"""Get the comment added for the specified merge request's job."""
|
|
397
|
+
with transaction(commit=False) as session:
|
|
398
|
+
comments = (
|
|
399
|
+
session.query(cls)
|
|
400
|
+
.filter(
|
|
401
|
+
Comments.merge_request_job == merge_request_job,
|
|
402
|
+
)
|
|
403
|
+
.first() # just one
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return comments
|
|
407
|
+
|
|
365
408
|
|
|
366
409
|
class Reactions(Base):
|
|
367
410
|
"""Store and count reactions received for
|
|
@@ -513,3 +556,32 @@ class Reactions(Base):
|
|
|
513
556
|
)
|
|
514
557
|
|
|
515
558
|
return reaction
|
|
559
|
+
|
|
560
|
+
@classmethod
|
|
561
|
+
@backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
|
|
562
|
+
def delete( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
563
|
+
cls,
|
|
564
|
+
forge: Forge,
|
|
565
|
+
project_id: int,
|
|
566
|
+
mr_iid: int,
|
|
567
|
+
job_id: int,
|
|
568
|
+
comment_id: str,
|
|
569
|
+
reaction_types: List[str],
|
|
570
|
+
) -> None:
|
|
571
|
+
"""Remove rows with given reaction types
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
forge: forge name
|
|
575
|
+
project_id: forge project id
|
|
576
|
+
mr_iid: merge request forge iid
|
|
577
|
+
job_id: forge job id
|
|
578
|
+
comment_id: forge comment id
|
|
579
|
+
reaction_type: a str iterable, ex. ['thumbsup', 'thumbsdown']
|
|
580
|
+
"""
|
|
581
|
+
for reaction_type in reaction_types:
|
|
582
|
+
reaction = cls.get_reaction_by_type(
|
|
583
|
+
forge, project_id, mr_iid, job_id, comment_id, reaction_type
|
|
584
|
+
)
|
|
585
|
+
with transaction(commit=True) as session:
|
|
586
|
+
session.delete(reaction)
|
|
587
|
+
session.flush()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from collections import Counter
|
|
5
|
+
|
|
6
|
+
import gitlab
|
|
7
|
+
|
|
8
|
+
from logdetective.server.models import TimePeriod
|
|
9
|
+
from logdetective.server.database.models import (
|
|
10
|
+
Comments,
|
|
11
|
+
Reactions,
|
|
12
|
+
GitlabMergeRequestJobs,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def collect_emojis(gitlab_conn: gitlab.Gitlab, period: TimePeriod):
|
|
17
|
+
"""
|
|
18
|
+
Collect emoji feedback from logdetective comments saved in database.
|
|
19
|
+
Check only comments created in the last given period of time.
|
|
20
|
+
"""
|
|
21
|
+
comments = Comments.get_since(period.get_period_start_time())
|
|
22
|
+
comments_for_gitlab_connection = [
|
|
23
|
+
comment for comment in comments if comment.forge == gitlab_conn.url
|
|
24
|
+
]
|
|
25
|
+
await collect_emojis_in_comments(comments_for_gitlab_connection, gitlab_conn)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def collect_emojis_for_mr(
|
|
29
|
+
project_id: int, mr_iid: int, gitlab_conn: gitlab.Gitlab
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Collect emoji feedback from logdetective comments in the specified MR.
|
|
33
|
+
"""
|
|
34
|
+
mr_jobs = GitlabMergeRequestJobs.get_by_mr_iid(gitlab_conn.url, project_id, mr_iid)
|
|
35
|
+
comments = [Comments.get_by_mr_job(mr_job) for mr_job in mr_jobs]
|
|
36
|
+
await collect_emojis_in_comments(comments, gitlab_conn)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def collect_emojis_in_comments( # pylint: disable=too-many-locals
|
|
40
|
+
comments: List[Comments], gitlab_conn: gitlab.Gitlab
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Collect emoji feedback from specified logdetective comments.
|
|
44
|
+
"""
|
|
45
|
+
projects = {}
|
|
46
|
+
mrs = {}
|
|
47
|
+
for comment in comments:
|
|
48
|
+
mr_job_db = GitlabMergeRequestJobs.get_by_id(comment.merge_request_job_id)
|
|
49
|
+
if mr_job_db.id not in projects:
|
|
50
|
+
projects[mr_job_db.id] = project = await asyncio.to_thread(
|
|
51
|
+
gitlab_conn.projects.get, mr_job_db.project_id
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
project = projects[mr_job_db.id]
|
|
55
|
+
mr_iid = mr_job_db.mr_iid
|
|
56
|
+
if mr_iid not in mrs:
|
|
57
|
+
mrs[mr_iid] = mr = await asyncio.to_thread(
|
|
58
|
+
project.mergerequests.get, mr_iid
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
mr = mrs[mr_iid]
|
|
62
|
+
|
|
63
|
+
discussion = mr.discussions.get(comment.comment_id)
|
|
64
|
+
|
|
65
|
+
# Get the ID of the first note
|
|
66
|
+
note_id = discussion.attributes["notes"][0]["id"]
|
|
67
|
+
note = mr.notes.get(note_id)
|
|
68
|
+
|
|
69
|
+
emoji_counts = Counter(emoji.name for emoji in note.awardemojis.list())
|
|
70
|
+
|
|
71
|
+
# keep track of not updated reactions
|
|
72
|
+
# because we need to remove them
|
|
73
|
+
old_emojis = [
|
|
74
|
+
reaction.reaction_type
|
|
75
|
+
for reaction in Reactions.get_all_reactions(
|
|
76
|
+
comment.forge,
|
|
77
|
+
mr_job_db.project_id,
|
|
78
|
+
mr_job_db.mr_iid,
|
|
79
|
+
mr_job_db.job_id,
|
|
80
|
+
comment.comment_id,
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
for key, value in emoji_counts.items():
|
|
84
|
+
Reactions.create_or_update(
|
|
85
|
+
comment.forge,
|
|
86
|
+
mr_job_db.project_id,
|
|
87
|
+
mr_job_db.mr_iid,
|
|
88
|
+
mr_job_db.job_id,
|
|
89
|
+
comment.comment_id,
|
|
90
|
+
key,
|
|
91
|
+
value,
|
|
92
|
+
)
|
|
93
|
+
if key in old_emojis:
|
|
94
|
+
old_emojis.remove(key)
|
|
95
|
+
|
|
96
|
+
# not updated reactions has been removed, drop them
|
|
97
|
+
Reactions.delete(
|
|
98
|
+
comment.forge,
|
|
99
|
+
mr_job_db.project_id,
|
|
100
|
+
mr_job_db.mr_iid,
|
|
101
|
+
mr_job_db.job_id,
|
|
102
|
+
comment.comment_id,
|
|
103
|
+
old_emojis,
|
|
104
|
+
)
|