logdetective 2.9.0__tar.gz → 2.11.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.
Files changed (40) hide show
  1. {logdetective-2.9.0 → logdetective-2.11.0}/PKG-INFO +3 -3
  2. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/emoji.py +46 -48
  3. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/models.py +8 -41
  4. {logdetective-2.9.0 → logdetective-2.11.0}/pyproject.toml +3 -3
  5. {logdetective-2.9.0 → logdetective-2.11.0}/LICENSE +0 -0
  6. {logdetective-2.9.0 → logdetective-2.11.0}/README.md +0 -0
  7. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/__init__.py +0 -0
  8. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/constants.py +0 -0
  9. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/drain3.ini +0 -0
  10. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/extractors.py +0 -0
  11. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/logdetective.py +0 -0
  12. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/models.py +0 -0
  13. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/prompts-summary-first.yml +0 -0
  14. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/prompts-summary-only.yml +0 -0
  15. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/prompts.yml +0 -0
  16. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/remote_log.py +0 -0
  17. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/__init__.py +0 -0
  18. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/compressors.py +0 -0
  19. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/config.py +0 -0
  20. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/__init__.py +0 -0
  21. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/base.py +0 -0
  22. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/models/__init__.py +0 -0
  23. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/models/exceptions.py +0 -0
  24. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/models/koji.py +0 -0
  25. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/models/merge_request_jobs.py +0 -0
  26. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/database/models/metrics.py +0 -0
  27. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/exceptions.py +0 -0
  28. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/gitlab.py +0 -0
  29. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/koji.py +0 -0
  30. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/llm.py +0 -0
  31. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/metric.py +0 -0
  32. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/plot.py +0 -0
  33. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/server.py +0 -0
  34. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/templates/base_response.html.j2 +0 -0
  35. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/templates/gitlab_full_comment.md.j2 +0 -0
  36. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/templates/gitlab_short_comment.md.j2 +0 -0
  37. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/server/utils.py +0 -0
  38. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/skip_snippets.yml +0 -0
  39. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective/utils.py +0 -0
  40. {logdetective-2.9.0 → logdetective-2.11.0}/logdetective.1.asciidoc +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logdetective
3
- Version: 2.9.0
3
+ Version: 2.11.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
  License-File: LICENSE
@@ -28,12 +28,12 @@ Requires-Dist: aiolimiter (>=1.0.0,<2.0.0) ; extra == "server"
28
28
  Requires-Dist: aioresponses (>=0.7.8,<0.8.0) ; extra == "testing"
29
29
  Requires-Dist: alembic (>=1.13.3,<2.0.0) ; extra == "server" or extra == "server-testing"
30
30
  Requires-Dist: asciidoc[testing] (>=10.2.1,<11.0.0) ; extra == "testing"
31
- Requires-Dist: asyncpg (>=0.30.0,<0.31.0) ; extra == "server" or extra == "server-testing"
31
+ Requires-Dist: asyncpg (>=0.30.0,<1.0.0) ; extra == "server" or extra == "server-testing"
32
32
  Requires-Dist: backoff (==2.2.1) ; extra == "server" or extra == "server-testing"
33
33
  Requires-Dist: drain3 (>=0.9.11,<0.10.0)
34
34
  Requires-Dist: fastapi (>=0.111.1,<1.0.0) ; extra == "server" or extra == "server-testing"
35
35
  Requires-Dist: flexmock (>=0.12.2,<0.13.0) ; extra == "testing"
36
- Requires-Dist: huggingface-hub (>0.23.2,<0.35.0)
36
+ Requires-Dist: huggingface-hub (>=0.23.0,<1.4.0)
37
37
  Requires-Dist: koji (>=1.35.0,<2.0.0) ; extra == "server" or extra == "server-testing"
38
38
  Requires-Dist: llama-cpp-python (>0.2.56,!=0.2.86,<1.0.0)
39
39
  Requires-Dist: matplotlib (>=3.8.4,<4.0.0) ; extra == "server" or extra == "server-testing"
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
 
3
- from typing import List, Callable
3
+ from typing import List
4
4
  from collections import Counter
5
5
 
6
6
  import gitlab
@@ -49,25 +49,6 @@ async def collect_emojis_for_mr(
49
49
  await collect_emojis_in_comments(comments, gitlab_conn)
50
50
 
51
51
 
52
- async def _handle_gitlab_operation(func: Callable, *args):
53
- """
54
- It handles errors for the specified GitLab operation.
55
- After executing it in a separate thread.
56
- """
57
- try:
58
- return await asyncio.to_thread(func, *args)
59
- except (gitlab.GitlabError, gitlab.GitlabGetError) as e:
60
- log_msg = f"Error during GitLab operation {func}{args}: {e}"
61
- if "Not Found" in str(e):
62
- LOG.error(log_msg)
63
- else:
64
- LOG.exception(log_msg)
65
- except Exception as e: # pylint: disable=broad-exception-caught
66
- LOG.exception(
67
- "Unexpected error during GitLab operation %s(%s): %s", func, args, e
68
- )
69
-
70
-
71
52
  async def collect_emojis_in_comments( # pylint: disable=too-many-locals
72
53
  comments: List[Comments], gitlab_conn: gitlab.Gitlab
73
54
  ):
@@ -80,37 +61,54 @@ async def collect_emojis_in_comments( # pylint: disable=too-many-locals
80
61
  mr_job_db = await GitlabMergeRequestJobs.get_by_id(comment.merge_request_job_id)
81
62
  if not mr_job_db:
82
63
  continue
83
- if mr_job_db.id not in projects:
84
- project = await _handle_gitlab_operation(
85
- gitlab_conn.projects.get, mr_job_db.project_id
86
- )
87
- if not project:
88
- continue
89
- projects[mr_job_db.id] = project
90
- else:
91
- project = projects[mr_job_db.id]
92
- merge_request_iid = mr_job_db.mr_iid
93
- if merge_request_iid not in merge_requests:
94
- merge_request = await _handle_gitlab_operation(
95
- project.mergerequests.get, merge_request_iid
64
+ try:
65
+ if mr_job_db.id not in projects:
66
+ project = await asyncio.to_thread(
67
+ gitlab_conn.projects.get, mr_job_db.project_id
68
+ )
69
+
70
+ projects[mr_job_db.id] = project
71
+ else:
72
+ project = projects[mr_job_db.id]
73
+ merge_request_iid = mr_job_db.mr_iid
74
+ if merge_request_iid not in merge_requests:
75
+ merge_request = await asyncio.to_thread(
76
+ project.mergerequests.get, merge_request_iid
77
+ )
78
+
79
+ merge_requests[merge_request_iid] = merge_request
80
+ else:
81
+ merge_request = merge_requests[merge_request_iid]
82
+
83
+ discussion = await asyncio.to_thread(
84
+ merge_request.discussions.get, comment.comment_id
96
85
  )
97
- if not merge_request:
98
- continue
99
- merge_requests[merge_request_iid] = merge_request
100
- else:
101
- merge_request = merge_requests[merge_request_iid]
102
86
 
103
- discussion = await _handle_gitlab_operation(
104
- merge_request.discussions.get, comment.comment_id
105
- )
106
- if not discussion:
107
- continue
87
+ # Get the ID of the first note
88
+ if "notes" not in discussion.attributes or len(discussion.attributes["notes"]) == 0:
89
+ LOG.warning(
90
+ "No notes were found in comment %s in merge request %d",
91
+ comment.comment_id,
92
+ merge_request_iid,
93
+ )
94
+ continue
108
95
 
109
- # Get the ID of the first note
110
- note_id = discussion.attributes["notes"][0]["id"]
111
- note = await _handle_gitlab_operation(merge_request.notes.get, note_id)
112
- if not note:
113
- continue
96
+ note_id = discussion.attributes["notes"][0]["id"]
97
+ note = await asyncio.to_thread(merge_request.notes.get, note_id)
98
+
99
+ # Log warning with full stack trace, in case we can't find the right
100
+ # discussion, merge request or project.
101
+ # All of these objects can be lost, and we shouldn't treat as an error.
102
+ # Other exceptions are raised.
103
+ except gitlab.GitlabError as e:
104
+ if e.response_code == 404:
105
+ LOG.warning(
106
+ "Couldn't retrieve emoji counts for comment %s due to GitlabError",
107
+ comment.comment_id, exc_info=True)
108
+ continue
109
+ LOG.error("Error encountered while processing emoji counts for GitLab comment %s",
110
+ comment.comment_id, exc_info=True)
111
+ raise
114
112
 
115
113
  emoji_counts = Counter(emoji.name for emoji in note.awardemojis.list())
116
114
 
@@ -10,6 +10,7 @@ from pydantic import (
10
10
  field_validator,
11
11
  NonNegativeFloat,
12
12
  HttpUrl,
13
+ PrivateAttr,
13
14
  )
14
15
 
15
16
  import aiohttp
@@ -183,8 +184,8 @@ class InferenceConfig(BaseModel): # pylint: disable=too-many-instance-attribute
183
184
  user_role: str = USER_ROLE_DEFAULT
184
185
  system_role: str = SYSTEM_ROLE_DEFAULT
185
186
  llm_api_timeout: float = 15.0
186
- _http_session: aiohttp.ClientSession = None
187
- _limiter: AsyncLimiter = AsyncLimiter(LLM_DEFAULT_REQUESTS_PER_MINUTE)
187
+ _limiter: AsyncLimiter = PrivateAttr(
188
+ default_factory=lambda: AsyncLimiter(LLM_DEFAULT_REQUESTS_PER_MINUTE))
188
189
 
189
190
  def __init__(self, data: Optional[dict] = None):
190
191
  super().__init__()
@@ -207,40 +208,6 @@ class InferenceConfig(BaseModel): # pylint: disable=too-many-instance-attribute
207
208
  self.llm_api_timeout = data.get("llm_api_timeout", 15.0)
208
209
  self._limiter = AsyncLimiter(self._requests_per_minute)
209
210
 
210
- def __del__(self):
211
- # Close connection when this object is destroyed
212
- if self._http_session:
213
- try:
214
- loop = asyncio.get_running_loop()
215
- loop.create_task(self._http_session.close())
216
- except RuntimeError:
217
- # No loop running, so create one to close the session
218
- loop = asyncio.new_event_loop()
219
- loop.run_until_complete(self._http_session.close())
220
- loop.close()
221
- except Exception: # pylint: disable=broad-exception-caught
222
- # We should only get here if we're shutting down, so we don't
223
- # really care if the close() completes cleanly.
224
- pass
225
-
226
- def get_http_session(self):
227
- """Return the internal HTTP session so it can be used to contect the
228
- LLM server. May be used as a context manager."""
229
-
230
- # Create the session on the first attempt. We need to do this "lazily"
231
- # because it needs to happen once the event loop is running, even
232
- # though the initialization itself is synchronous.
233
- if not self._http_session:
234
- self._http_session = aiohttp.ClientSession(
235
- base_url=self.url,
236
- timeout=aiohttp.ClientTimeout(
237
- total=self.http_timeout,
238
- connect=3.07,
239
- ),
240
- )
241
-
242
- return self._http_session
243
-
244
211
  def get_limiter(self):
245
212
  """Return the limiter object so it can be used as a context manager"""
246
213
  return self._limiter
@@ -254,7 +221,7 @@ class ExtractorConfig(BaseModel):
254
221
  max_snippet_len: int = 2000
255
222
  csgrep: bool = False
256
223
 
257
- _extractors: List[Extractor] = []
224
+ _extractors: List[Extractor] = PrivateAttr(default_factory=list)
258
225
 
259
226
  def _setup_extractors(self):
260
227
  """Initialize extractors with common settings."""
@@ -322,8 +289,8 @@ class GitLabInstanceConfig(BaseModel): # pylint: disable=too-many-instance-attr
322
289
  webhook_secrets: Optional[List[str]] = None
323
290
 
324
291
  timeout: float = 5.0
325
- _conn: Gitlab = None
326
- _http_session: aiohttp.ClientSession = None
292
+ _conn: Gitlab | None = PrivateAttr(default=None)
293
+ _http_session: aiohttp.ClientSession | None = PrivateAttr(default=None)
327
294
 
328
295
  # Maximum size of artifacts.zip in MiB. (default: 300 MiB)
329
296
  max_artifact_size: int = 300 * 1024 * 1024
@@ -409,8 +376,8 @@ class KojiInstanceConfig(BaseModel):
409
376
  xmlrpc_url: str = ""
410
377
  tokens: List[str] = []
411
378
 
412
- _conn: Optional[koji.ClientSession] = None
413
- _callbacks: defaultdict[int, set[str]] = defaultdict(set)
379
+ _conn: Optional[koji.ClientSession] = PrivateAttr(default=None)
380
+ _callbacks: defaultdict[int, set[str]] = PrivateAttr(default_factory=lambda: defaultdict(set))
414
381
 
415
382
  def __init__(self, name: str, data: Optional[dict] = None):
416
383
  super().__init__()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "logdetective"
3
- version = "2.9.0"
3
+ version = "2.11.0"
4
4
  description = "Log using LLM AI to search for build/test failures and provide ideas for fixing these."
5
5
  authors = ["Jiri Podivin <jpodivin@gmail.com>"]
6
6
  license = "Apache-2.0"
@@ -34,7 +34,7 @@ issues = "https://github.com/fedora-copr/logdetective/issues"
34
34
  python = "^3.11"
35
35
  llama-cpp-python = ">0.2.56,!=0.2.86,<1.0.0"
36
36
  drain3 = ">=0.9.11,<0.10.0"
37
- huggingface-hub = ">0.23.2,<0.35.0"
37
+ huggingface-hub = ">=0.23.0,<1.4.0"
38
38
  # rawhide has numpy 2, F40 and F41 are still on 1.26
39
39
  # we need to support both versions
40
40
  numpy = ">=1.26.0"
@@ -59,7 +59,7 @@ pytest-mock = {version = ">=3.14.1,<4.0.0", optional = true}
59
59
  flexmock = {version = ">=0.12.2,<0.13.0", optional = true}
60
60
  pytest = {version = ">=8.4.1,<9.0.0", optional = true}
61
61
  pytest-cov = {version = "^7.0.0", extras = ["testing"], optional = true}
62
- asyncpg = {version = "^0.30.0", optional = true}
62
+ asyncpg = {version = ">=0.30.0,<1.0.0", optional = true}
63
63
  asciidoc = {version = "^10.2.1", optional = true, extras = ["testing"]}
64
64
 
65
65
  [tool.poetry.extras]
File without changes
File without changes