logdetective 2.0.1__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.
Files changed (32) hide show
  1. logdetective/extractors.py +134 -23
  2. logdetective/logdetective.py +39 -23
  3. logdetective/models.py +26 -0
  4. logdetective/prompts-summary-first.yml +0 -2
  5. logdetective/prompts.yml +0 -3
  6. logdetective/server/compressors.py +7 -10
  7. logdetective/server/config.py +3 -2
  8. logdetective/server/database/base.py +31 -26
  9. logdetective/server/database/models/__init__.py +2 -2
  10. logdetective/server/database/models/exceptions.py +4 -0
  11. logdetective/server/database/models/koji.py +47 -30
  12. logdetective/server/database/models/merge_request_jobs.py +205 -186
  13. logdetective/server/database/models/metrics.py +87 -61
  14. logdetective/server/emoji.py +57 -55
  15. logdetective/server/exceptions.py +4 -0
  16. logdetective/server/gitlab.py +18 -11
  17. logdetective/server/llm.py +19 -10
  18. logdetective/server/metric.py +18 -13
  19. logdetective/server/models.py +65 -48
  20. logdetective/server/plot.py +13 -11
  21. logdetective/server/server.py +52 -30
  22. logdetective/server/templates/base_response.html.j2 +59 -0
  23. logdetective/server/templates/gitlab_full_comment.md.j2 +58 -53
  24. logdetective/server/templates/gitlab_short_comment.md.j2 +52 -47
  25. logdetective/server/utils.py +15 -27
  26. logdetective/utils.py +115 -49
  27. {logdetective-2.0.1.dist-info → logdetective-2.11.0.dist-info}/METADATA +95 -21
  28. logdetective-2.11.0.dist-info/RECORD +40 -0
  29. {logdetective-2.0.1.dist-info → logdetective-2.11.0.dist-info}/WHEEL +1 -1
  30. logdetective-2.0.1.dist-info/RECORD +0 -39
  31. {logdetective-2.0.1.dist-info → logdetective-2.11.0.dist-info}/entry_points.txt +0 -0
  32. {logdetective-2.0.1.dist-info → logdetective-2.11.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,6 +1,9 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
1
3
  from datetime import datetime, timedelta, timezone
2
- from sqlalchemy import Column, BigInteger, DateTime, ForeignKey, Integer, String
3
- from sqlalchemy.orm import relationship
4
+ from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, select
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+
4
7
  from sqlalchemy.exc import OperationalError
5
8
  import backoff
6
9
 
@@ -12,6 +15,7 @@ from logdetective.server.database.models.exceptions import (
12
15
  KojiTaskNotFoundError,
13
16
  KojiTaskNotAnalyzedError,
14
17
  KojiTaskAnalysisTimeoutError,
18
+ AnalyzeRequestMetricsNotFroundError,
15
19
  )
16
20
  from logdetective.server.models import KojiStagedResponse
17
21
 
@@ -21,42 +25,47 @@ class KojiTaskAnalysis(Base):
21
25
 
22
26
  __tablename__ = "koji_task_analysis"
23
27
 
24
- id = Column(Integer, primary_key=True)
25
- koji_instance = Column(String(255), nullable=False, index=True)
26
- task_id = Column(BigInteger, nullable=False, index=True, unique=True)
27
- log_file_name = Column(String(255), nullable=False, index=True)
28
- request_received_at = Column(
29
- DateTime,
28
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
29
+ koji_instance: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
30
+ task_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True, unique=True)
31
+ log_file_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
32
+ request_received_at: Mapped[datetime] = mapped_column(
33
+ DateTime(timezone=True),
30
34
  nullable=False,
31
35
  index=True,
32
36
  default=datetime.now(timezone.utc),
33
37
  comment="Timestamp when the request was received",
34
38
  )
35
- response_id = Column(
39
+ response_id: Mapped[Optional[int]] = mapped_column(
36
40
  Integer,
37
41
  ForeignKey("analyze_request_metrics.id"),
38
42
  nullable=True,
39
43
  index=False,
40
44
  comment="The id of the analyze request metrics for this task",
41
45
  )
42
- response = relationship("AnalyzeRequestMetrics")
46
+ response: Mapped[Optional["AnalyzeRequestMetrics"]] = relationship(
47
+ "AnalyzeRequestMetrics",
48
+ back_populates="koji_tasks"
49
+ )
43
50
 
44
51
  @classmethod
45
52
  @backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
46
- def create_or_restart(cls, koji_instance: str, task_id: int, log_file_name: str):
53
+ async def create_or_restart(
54
+ cls, koji_instance: str, task_id: int, log_file_name: str
55
+ ):
47
56
  """Create a new koji task analysis"""
48
- with transaction(commit=True) as session:
57
+ query = select(cls).filter(
58
+ cls.koji_instance == koji_instance, cls.task_id == task_id
59
+ )
60
+ async with transaction(commit=True) as session:
49
61
  # Check if the task analysis already exists
50
- koji_task_analysis = (
51
- session.query(cls)
52
- .filter_by(koji_instance=koji_instance, task_id=task_id)
53
- .first()
54
- )
62
+ query_result = await session.execute(query)
63
+ koji_task_analysis = query_result.first()
55
64
  if koji_task_analysis:
56
65
  # If it does, update the request_received_at timestamp
57
66
  koji_task_analysis.request_received_at = datetime.now(timezone.utc)
58
67
  session.add(koji_task_analysis)
59
- session.flush()
68
+ await session.flush()
60
69
  return
61
70
 
62
71
  # If it doesn't, create a new one
@@ -65,14 +74,22 @@ class KojiTaskAnalysis(Base):
65
74
  koji_task_analysis.task_id = task_id
66
75
  koji_task_analysis.log_file_name = log_file_name
67
76
  session.add(koji_task_analysis)
68
- session.flush()
77
+ await session.flush()
69
78
 
70
79
  @classmethod
71
80
  @backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
72
- def add_response(cls, task_id: int, metric_id: int):
81
+ async def add_response(cls, task_id: int, metric_id: int):
73
82
  """Add a response to a koji task analysis"""
74
- with transaction(commit=True) as session:
75
- koji_task_analysis = session.query(cls).filter_by(task_id=task_id).first()
83
+ query = select(cls).filter(cls.task_id == task_id)
84
+ metrics_query = select(AnalyzeRequestMetrics).filter(
85
+ AnalyzeRequestMetrics.id == metric_id
86
+ )
87
+ async with transaction(commit=True) as session:
88
+ query_result = await session.execute(query)
89
+ koji_task_analysis = query_result.scalars().first()
90
+ if not koji_task_analysis:
91
+ raise AnalyzeRequestMetricsNotFroundError(
92
+ f"No AnalyzeRequestMetrics record found for id {metric_id}")
76
93
  # Ensure that the task analysis doesn't already have a response
77
94
  if koji_task_analysis.response:
78
95
  # This is probably due to an analysis that took so long that
@@ -81,20 +98,20 @@ class KojiTaskAnalysis(Base):
81
98
  # returned to the consumer, so we'll just drop this extra one
82
99
  # on the floor and keep the one saved in the database.
83
100
  return
84
-
85
- metric = (
86
- session.query(AnalyzeRequestMetrics).filter_by(id=metric_id).first()
87
- )
101
+ metrics_query_result = await session.execute(metrics_query)
102
+ metric = metrics_query_result.scalars().first()
88
103
  koji_task_analysis.response = metric
89
104
  session.add(koji_task_analysis)
90
- session.flush()
105
+ await session.flush()
91
106
 
92
107
  @classmethod
93
108
  @backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
94
- def get_response_by_task_id(cls, task_id: int) -> KojiStagedResponse:
109
+ async def get_response_by_task_id(cls, task_id: int) -> KojiStagedResponse:
95
110
  """Get a koji task analysis by task id"""
96
- with transaction(commit=False) as session:
97
- koji_task_analysis = session.query(cls).filter_by(task_id=task_id).first()
111
+ query = select(cls).filter(cls.task_id == task_id)
112
+ async with transaction(commit=False) as session:
113
+ query_result = await session.execute(query)
114
+ koji_task_analysis = query_result.scalars().first()
98
115
  if not koji_task_analysis:
99
116
  raise KojiTaskNotFoundError(f"Task {task_id} not yet analyzed")
100
117