logdetective 2.7.0__py3-none-any.whl → 2.9.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.
@@ -36,20 +36,17 @@ class TextCompressor:
36
36
  zip_buffer.seek(0)
37
37
  return zip_buffer.getvalue()
38
38
 
39
- def unzip(self, zip_data: Union[bytes, io.BytesIO]) -> str:
39
+ def unzip(self, zip_data: bytes) -> Dict[str, str]:
40
40
  """
41
41
  Uncompress data created by TextCompressor.zip().
42
42
 
43
43
  Args:
44
- zip_data: A zipped stream of bytes or BytesIO object
44
+ zip_data: A zipped stream of bytes
45
45
 
46
46
  Returns:
47
47
  {file_name: str}: The decompressed content as a dict of file names and UTF-8 strings
48
48
  """
49
- if isinstance(zip_data, bytes):
50
- zip_buffer = io.BytesIO(zip_data)
51
- else:
52
- zip_buffer = zip_data
49
+ zip_buffer = io.BytesIO(zip_data)
53
50
 
54
51
  content = {}
55
52
  with zipfile.ZipFile(zip_buffer, "r") as zip_file:
@@ -95,12 +92,12 @@ class RemoteLogCompressor:
95
92
  return self.zip_text(content_text)
96
93
 
97
94
  @classmethod
98
- def unzip(cls, zip_data: Union[bytes, io.BytesIO]) -> str:
95
+ def unzip(cls, zip_data: bytes) -> str:
99
96
  """
100
97
  Uncompress the zipped content of the remote log.
101
98
 
102
99
  Args:
103
- zip_data: Compressed data as bytes or BytesIO
100
+ zip_data: Compressed data as bytes
104
101
 
105
102
  Returns:
106
103
  str: The decompressed log content
@@ -147,13 +144,13 @@ class LLMResponseCompressor:
147
144
 
148
145
  @classmethod
149
146
  def unzip(
150
- cls, zip_data: Union[bytes, io.BytesIO]
147
+ cls, zip_data: bytes
151
148
  ) -> Union[StagedResponse, Response]:
152
149
  """
153
150
  Uncompress the zipped content of the LLM response.
154
151
 
155
152
  Args:
156
- zip_data: Compressed data as bytes or BytesIO
153
+ zip_data: Compressed data as bytes
157
154
 
158
155
  Returns:
159
156
  Union[StagedResponse, Response]: The decompressed (partial) response object,
@@ -11,3 +11,7 @@ class KojiTaskNotAnalyzedError(Exception):
11
11
 
12
12
  class KojiTaskAnalysisTimeoutError(Exception):
13
13
  """Exception raised when a koji task analysis has timed out"""
14
+
15
+
16
+ class AnalyzeRequestMetricsNotFroundError(Exception):
17
+ """Exception raised when AnalyzeRequestMetrics is not found"""
@@ -15,6 +15,7 @@ from logdetective.server.database.models.exceptions import (
15
15
  KojiTaskNotFoundError,
16
16
  KojiTaskNotAnalyzedError,
17
17
  KojiTaskAnalysisTimeoutError,
18
+ AnalyzeRequestMetricsNotFroundError,
18
19
  )
19
20
  from logdetective.server.models import KojiStagedResponse
20
21
 
@@ -86,6 +87,9 @@ class KojiTaskAnalysis(Base):
86
87
  async with transaction(commit=True) as session:
87
88
  query_result = await session.execute(query)
88
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}")
89
93
  # Ensure that the task analysis doesn't already have a response
90
94
  if koji_task_analysis.response:
91
95
  # This is probably due to an analysis that took so long that
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
- import io
3
2
  import enum
4
3
  import datetime
5
4
  from typing import Optional, List, Self, Tuple, TYPE_CHECKING
@@ -107,7 +106,7 @@ class AnalyzeRequestMetrics(Base):
107
106
  async def create(
108
107
  cls,
109
108
  endpoint: EndpointType,
110
- compressed_log: io.BytesIO,
109
+ compressed_log: bytes,
111
110
  request_received_at: Optional[datetime.datetime] = None,
112
111
  ) -> int:
113
112
  """Create AnalyzeRequestMetrics new line
@@ -42,6 +42,10 @@ async def collect_emojis_for_mr(
42
42
  mr_jobs = await GitlabMergeRequestJobs.get_by_mr_iid(url, project_id, mr_iid) or []
43
43
 
44
44
  comments = [await Comments.get_by_mr_job(mr_job) for mr_job in mr_jobs]
45
+ # Filter all cases when no comments were found. This shouldn't happen if the database
46
+ # is in good order. But checking for it can't hurt.
47
+ comments = [comment for comment in comments if isinstance(comment, Comments)]
48
+
45
49
  await collect_emojis_in_comments(comments, gitlab_conn)
46
50
 
47
51
 
@@ -31,3 +31,7 @@ class LogDetectiveConnectionError(LogDetectiveKojiException):
31
31
 
32
32
  class LogsTooLargeError(LogDetectiveKojiException):
33
33
  """The log archive exceeds the configured maximum size"""
34
+
35
+
36
+ class LogDetectiveMetricsError(LogDetectiveException):
37
+ """Exception was encountered while recording metrics"""
@@ -1,4 +1,3 @@
1
- import io
2
1
  import inspect
3
2
  import datetime
4
3
 
@@ -13,14 +12,15 @@ from logdetective.remote_log import RemoteLog
13
12
  from logdetective.server.config import LOG
14
13
  from logdetective.server.compressors import LLMResponseCompressor, RemoteLogCompressor
15
14
  from logdetective.server.database.models import EndpointType, AnalyzeRequestMetrics
15
+ from logdetective.server.exceptions import LogDetectiveMetricsError
16
16
 
17
17
 
18
18
  async def add_new_metrics(
19
- api_name: str,
19
+ api_name: EndpointType,
20
20
  url: Optional[str] = None,
21
21
  http_session: Optional[aiohttp.ClientSession] = None,
22
22
  received_at: Optional[datetime.datetime] = None,
23
- compressed_log_content: Optional[io.BytesIO] = None,
23
+ compressed_log_content: Optional[bytes] = None,
24
24
  ) -> int:
25
25
  """Add a new database entry for a received request.
26
26
 
@@ -29,6 +29,10 @@ async def add_new_metrics(
29
29
  and the log (in a zip format) for which analysis is requested.
30
30
  """
31
31
  if not compressed_log_content:
32
+ if not (url and http_session):
33
+ raise LogDetectiveMetricsError(
34
+ f"""Remote log can not be retrieved without URL and http session.
35
+ URL: {url}, http session:{http_session}""")
32
36
  remote_log = RemoteLog(url, http_session)
33
37
  compressed_log_content = await RemoteLogCompressor(remote_log).zip_content()
34
38
 
@@ -109,7 +113,8 @@ def track_request(name=None):
109
113
  async def async_decorated_function(*args, **kwargs):
110
114
  log_url = kwargs["build_log"].url
111
115
  metrics_id = await add_new_metrics(
112
- name if name else f.__name__, log_url, kwargs["http_session"]
116
+ api_name=EndpointType(name if name else f.__name__),
117
+ url=log_url, http_session=kwargs["http_session"]
113
118
  )
114
119
  response = await f(*args, **kwargs)
115
120
  await update_metrics(metrics_id, response)
@@ -500,8 +500,8 @@ class LogConfig(BaseModel):
500
500
  class GeneralConfig(BaseModel):
501
501
  """General config options for Log Detective"""
502
502
 
503
- packages: List[str] = None
504
- excluded_packages: List[str] = None
503
+ packages: List[str] = []
504
+ excluded_packages: List[str] = []
505
505
  devmode: bool = False
506
506
  sentry_dsn: HttpUrl | None = None
507
507
  collect_emojis_interval: int = 60 * 60 # seconds
@@ -568,7 +568,8 @@ class TimePeriod(BaseModel):
568
568
  @model_validator(mode="before")
569
569
  @classmethod
570
570
  def check_exclusive_fields(cls, data):
571
- """Check that only one key between weeks, days and hours is defined"""
571
+ """Check that only one key between weeks, days and hours is defined,
572
+ if no period is specified, fall back to 2 days."""
572
573
  if isinstance(data, dict):
573
574
  how_many_fields = sum(
574
575
  1
@@ -594,6 +595,7 @@ class TimePeriod(BaseModel):
594
595
 
595
596
  def get_time_period(self) -> datetime.timedelta:
596
597
  """Get the period of time represented by this input model.
598
+ Will default to 2 days, if no period is set.
597
599
 
598
600
  Returns:
599
601
  datetime.timedelta: The time period as a timedelta object.
@@ -605,10 +607,12 @@ class TimePeriod(BaseModel):
605
607
  delta = datetime.timedelta(days=self.days)
606
608
  elif self.hours:
607
609
  delta = datetime.timedelta(hours=self.hours)
610
+ else:
611
+ delta = datetime.timedelta(days=2)
608
612
  return delta
609
613
 
610
614
  def get_period_start_time(
611
- self, end_time: datetime.datetime = None
615
+ self, end_time: Optional[datetime.datetime] = None
612
616
  ) -> datetime.datetime:
613
617
  """Calculate the start time of this period based on the end time.
614
618
 
@@ -621,5 +625,5 @@ class TimePeriod(BaseModel):
621
625
  """
622
626
  time = end_time or datetime.datetime.now(datetime.timezone.utc)
623
627
  if time.tzinfo is None:
624
- end_time = end_time.replace(tzinfo=datetime.timezone.utc)
628
+ time = time.replace(tzinfo=datetime.timezone.utc)
625
629
  return time - self.get_time_period()
@@ -69,7 +69,7 @@ def create_time_series_arrays(
69
69
  plot_def: Definition,
70
70
  start_time: datetime.datetime,
71
71
  end_time: datetime.datetime,
72
- value_type: Optional[Union[int, float]] = int,
72
+ value_type: Optional[Union[type[int], type[float]]] = int,
73
73
  ) -> tuple[numpy.ndarray, numpy.ndarray]:
74
74
  """Create time series arrays from a dictionary of values.
75
75
 
@@ -139,7 +139,19 @@ def requires_token_when_set(authorization: Annotated[str | None, Header()] = Non
139
139
  raise HTTPException(status_code=401, detail="No token provided.")
140
140
 
141
141
 
142
- app = FastAPI(dependencies=[Depends(requires_token_when_set)], lifespan=lifespan)
142
+ app = FastAPI(
143
+ title="Log Detective",
144
+ contact={
145
+ "name": "Log Detective developers",
146
+ "url": "https://github.com/fedora-copr/logdetective",
147
+ "email": "copr-devel@lists.fedorahosted.org"
148
+ },
149
+ license_info={
150
+ "name": "Apache-2.0",
151
+ "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
152
+ },
153
+ version=get_version(),
154
+ dependencies=[Depends(requires_token_when_set)], lifespan=lifespan)
143
155
 
144
156
 
145
157
  @app.post("/analyze", response_model=Response)
@@ -302,7 +314,7 @@ async def analyze_koji_task(task_id: int, koji_instance_config: KojiInstanceConf
302
314
  # to retrieve the metric ID to associate it with the koji task analysis.
303
315
 
304
316
  metrics_id = await add_new_metrics(
305
- "analyze_koji_task",
317
+ EndpointType.ANALYZE_KOJI_TASK,
306
318
  log_text,
307
319
  received_at=datetime.datetime.now(datetime.timezone.utc),
308
320
  compressed_log_content=RemoteLogCompressor.zip_text(log_text),
@@ -357,10 +369,10 @@ async def async_log(msg):
357
369
  return msg
358
370
 
359
371
 
360
- @app.get("/version")
372
+ @app.get("/version", response_class=BasicResponse)
361
373
  async def get_version_wrapper():
362
374
  """Get the version of logdetective"""
363
- return get_version()
375
+ return BasicResponse(content=get_version())
364
376
 
365
377
 
366
378
  @app.post("/analyze/stream", response_class=StreamingResponse)
@@ -529,7 +541,7 @@ async def schedule_emoji_collection_for_mr(
529
541
  key = (forge, project_id, mr_iid)
530
542
 
531
543
  # FIXME: Look up the connection from the Forge # pylint: disable=fixme
532
- gitlab_conn = SERVER_CONFIG.gitlab.instances[forge.value]
544
+ gitlab_conn = SERVER_CONFIG.gitlab.instances[forge.value].get_connection()
533
545
 
534
546
  LOG.debug("Looking up emojis for %s, %d, %d", forge, project_id, mr_iid)
535
547
  await collect_emojis_for_mr(project_id, mr_iid, gitlab_conn)
@@ -4,20 +4,21 @@ Please know that the explanation was provided by AI and may be incorrect.
4
4
  In this case, we are {{ "%.2f" | format(certainty) }}% certain of the response {{ emoji_face }}.
5
5
  {% endif %}
6
6
 
7
- <b>Snippets:</b>
8
-
9
- <ul>
10
- {% for snippet in snippets %}
11
- <li>
12
- <b>Line {{ snippet.line_number }}:</b> <code>{{ snippet.text }}</code>
13
- {{ snippet.explanation.text }}
14
- </li>
15
- {% endfor %}
16
- </ul>
17
- <details>
7
+ <details open>
18
8
  <summary>Description</summary>
19
9
  {{ explanation }}
20
10
  </details>
11
+ <details>
12
+ {#
13
+ Formatted so that we don't trigger GitLab markdown
14
+ #}
15
+ <summary>Snippets</summary>
16
+ <ul>
17
+ {% for snippet in snippets -%}
18
+ <li><div><b>Line {{ snippet.line_number }}:</b> <code>{{ snippet.text | e }}</code><br>{{ snippet.explanation.text | e }}</div></li>
19
+ {%- endfor %}
20
+ </ul>
21
+ </details>
21
22
  <details>
22
23
  <summary>Logs</summary>
23
24
  <p>Log Detective analyzed the following logs files to provide an explanation:</p>
@@ -60,7 +61,8 @@ Please know that the explanation was provided by AI and may be incorrect.
60
61
 
61
62
  <hr>
62
63
 
63
- This comment was created by <a href="https://logdetective.com">Log Detective</a>.
64
+ This explanation was provided by AI (<a href="https://logdetective.com">Log Detective</a>).
65
+ Always review AI generated content prior to use.
64
66
  Was the provided feedback accurate and helpful?
65
67
  <br>
66
68
  Please vote with :thumbsup:
@@ -3,7 +3,8 @@ Please know that the explanation was provided by AI and may be incorrect.
3
3
  {% if certainty > 0 %}
4
4
  In this case, we are {{ "%.2f" | format(certainty) }}% certain of the response {{ emoji_face }}.
5
5
  {% endif %}
6
- <details>
6
+
7
+ <details open>
7
8
  <summary>Description</summary>
8
9
  {{ explanation }}
9
10
  </details>
@@ -49,7 +50,8 @@ Please know that the explanation was provided by AI and may be incorrect.
49
50
 
50
51
  <hr>
51
52
 
52
- This comment was created by <a href="https://logdetective.com">Log Detective</a>.
53
+ This explanation was provided by AI (<a href="https://logdetective.com">Log Detective</a>).
54
+ Always review AI generated content prior to use.
53
55
  Was the provided feedback accurate and helpful?
54
56
  <br>
55
57
  Please vote with :thumbsup:
@@ -3,7 +3,6 @@ from importlib.metadata import version
3
3
 
4
4
  import aiohttp
5
5
  from fastapi import HTTPException
6
- from fastapi.responses import Response as BasicResponse
7
6
 
8
7
  from logdetective.constants import SNIPPET_DELIMITER
9
8
  from logdetective.server.config import LOG
@@ -106,6 +105,6 @@ def construct_final_prompt(formatted_snippets: str, prompt_template: str) -> str
106
105
  return final_prompt
107
106
 
108
107
 
109
- def get_version() -> BasicResponse:
108
+ def get_version() -> str:
110
109
  """Obtain the version number using importlib"""
111
- return BasicResponse(content=version('logdetective'))
110
+ return version('logdetective')
logdetective/utils.py CHANGED
@@ -192,22 +192,21 @@ def format_snippets(snippets: list[str] | list[Tuple[int, str]]) -> str:
192
192
  Line number must be first element in the tuple. Mixed format of snippets
193
193
  is permitted, but may have impact on inference.
194
194
  """
195
- summary = ""
195
+ summary = "\n"
196
196
  for i, s in enumerate(snippets):
197
197
  if isinstance(s, tuple):
198
- summary += f"""
199
- Snippet No. {i} at line #{s[0]}:
200
-
201
- {s[1]}
202
- {SNIPPET_DELIMITER}
203
- """
198
+ line_number, snippet_content = s
199
+ header = f"Snippet No. {i} at line #{line_number}:"
204
200
  else:
205
- summary += f"""
206
- Snippet No. {i}:
207
-
208
- {s}
209
- {SNIPPET_DELIMITER}
210
- """
201
+ header = f"Snippet No. {i}:"
202
+ snippet_content = s
203
+ summary += (
204
+ f"{header}\n"
205
+ "\n"
206
+ f"{snippet_content}\n"
207
+ f"{SNIPPET_DELIMITER}\n"
208
+ f"\n"
209
+ )
211
210
  return summary
212
211
 
213
212
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logdetective
3
- Version: 2.7.0
3
+ Version: 2.9.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
@@ -325,6 +325,32 @@ podman-compose up server
325
325
 
326
326
  - Run Visual Stdio Code debug configuration named *Python Debug: Remote Attach*
327
327
 
328
+ Visual Studio Code CLI debugging
329
+ --------------------------------
330
+
331
+ When debugging the CLI application, the `./scripts/debug_runner.py` script can be used
332
+ as a stand in for stump script created during package installation.
333
+
334
+ Using `launch.json`, or similar alternative, arguments can be specified for testing.
335
+
336
+ Example:
337
+
338
+ ```
339
+ {
340
+ "version": "0.2.0",
341
+ "configurations": [
342
+ {
343
+ "name": "Python: Debug Installed Module",
344
+ "type": "debugpy",
345
+ "request": "launch",
346
+ "console": "integratedTerminal",
347
+ "program": "${workspaceFolder}/scripts/debug_runner.py",
348
+ "args": [<URL_OF_A_LOG>]
349
+ }
350
+ ]
351
+ }
352
+ ```
353
+
328
354
  Server
329
355
  ======
330
356
 
@@ -9,32 +9,32 @@ logdetective/prompts-summary-only.yml,sha256=8U9AMJV8ePW-0CoXOXlQoO92DAJDeutIT8n
9
9
  logdetective/prompts.yml,sha256=i3z6Jcb4ScVi7LsxOpDlKiXrcvql3qO_JnLzkAKMn1c,3870
10
10
  logdetective/remote_log.py,sha256=28QvdQiy7RBnd86EKCq_A75P21gSNlCbgxJe5XAe9MA,2258
11
11
  logdetective/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- logdetective/server/compressors.py,sha256=qzrT-BPSksXY6F2L6ger04GGrgdBsGOfK2YuCFRs0Q4,5427
12
+ logdetective/server/compressors.py,sha256=y4aFYJ_9CbYdKuAI39Kc9GQSdPN8cSJ2c_VAz3T47EE,5249
13
13
  logdetective/server/config.py,sha256=cKUmNCJyNyEid0bPTiUjr8CQuBYBab5bC79Axk2h0z8,2525
14
14
  logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  logdetective/server/database/base.py,sha256=HSV2tgye7iYTDzJD1Q5X7_nlLuTMIFP-hRVQMYxngHQ,2073
16
16
  logdetective/server/database/models/__init__.py,sha256=zoZMCt1_7tewDa6eEIIX_xrdN-tLegSiPNg5NiYaV3o,850
17
- logdetective/server/database/models/exceptions.py,sha256=AXQPZRgt-r2vboxP9SGYelngP6YIFpHlwELKcZ1FD3Y,384
18
- logdetective/server/database/models/koji.py,sha256=3soiJQ3L-H09cVO-BeRDEcP-HrSa-Z6qp3kwePUZsbo,6216
17
+ logdetective/server/database/models/exceptions.py,sha256=4ED7FSSA1liV9-7VIN2BwUiz6XlmP97Y1loKnsoNdD8,507
18
+ logdetective/server/database/models/koji.py,sha256=HNWxHYDxf4JN9K2ue8-V8dH-0XY5ZmxqH7Y9lAIbILA,6436
19
19
  logdetective/server/database/models/merge_request_jobs.py,sha256=MxiAVKQIsQMbFylBsmYBmVXYvid-4_5mwwXLfWdp6_w,19965
20
- logdetective/server/database/models/metrics.py,sha256=zzTRo67qwLyDj1GKbclZm1UXt1nNU-G662-y5XyGshE,15458
21
- logdetective/server/emoji.py,sha256=CoaAiZA_JgtUJe41YOsQRqd_QpgVeQ8szJt_bQ3d_JM,4837
22
- logdetective/server/exceptions.py,sha256=piV7wVKc-rw_pHrThbZbUjtmjuO5qUbjVNFwjdfcP3Q,864
20
+ logdetective/server/database/models/metrics.py,sha256=4xsUdbtlp5PI1-iJQc5Dd8EPDgVVplD9hJRWeRDn43k,15443
21
+ logdetective/server/emoji.py,sha256=nt3i_D5bk67RF4SlIetlLhLcgcxz9TEniC2iRYJx81w,5066
22
+ logdetective/server/exceptions.py,sha256=WN715KLL3ya6FiZ95v70VSbNuVhGuHFzxm2OeEPWQCw,981
23
23
  logdetective/server/gitlab.py,sha256=putpnf8PfGsCZJsqWZA1rMovRGnyagoQmgpKLqtA-aQ,16743
24
24
  logdetective/server/koji.py,sha256=LG1pRiKUFvYFRKzgQoUG3pUHfcEwMoaMNjUSMKw_pBA,5640
25
25
  logdetective/server/llm.py,sha256=bmA6LsV80OdO60q4WLoKuehuVDEYq-HhBAYcZeLfrv8,10150
26
- logdetective/server/metric.py,sha256=-gqdd6LBUs_K6Sk30TZ7FJYE7Htp8d-uhVY5NwYgD3g,4186
27
- logdetective/server/models.py,sha256=BoiiUYI6BcVDOqtIcUgNRsCyjcWNCKyEfVtYOLYgr1Y,20929
28
- logdetective/server/plot.py,sha256=lFKOTXlLJ3aZtZGYXceA4OqUt1z18BpLITTI39fOMx4,14685
29
- logdetective/server/server.py,sha256=Qx8mqkrRq-nlOnmOmICbaXxUWq2J60K4dHKiJtQJuE0,24938
26
+ logdetective/server/metric.py,sha256=wLOpgcAch3rwhPA5P2YWUeMNAPsvRGseRjH5HlTb7JM,4529
27
+ logdetective/server/models.py,sha256=V2QCKF_zB9FLaZjiBnUlmOZc5c8SsS4uhUmxrhqcJSY,21098
28
+ logdetective/server/plot.py,sha256=8LERgY3vQckaHZV2PZfOrZT8CjCAiji57QCmRW24Rfo,14697
29
+ logdetective/server/server.py,sha256=JueU-5c8t9h1CZy4gtoEeT8VSEirpeS0K3wrfqTPvAc,25381
30
30
  logdetective/server/templates/base_response.html.j2,sha256=BJGGV_Xb0Lnue8kq32oG9lI5CQDf9vce7HMYsP-Pvb4,2040
31
- logdetective/server/templates/gitlab_full_comment.md.j2,sha256=hSWEj_a7KZpzfbgoPhWA9KBwa25daDCb6S3cuBqclss,2143
32
- logdetective/server/templates/gitlab_short_comment.md.j2,sha256=d396HR2DJuS8XLu2FNgVAg1CrOW8_LySQX2f6opOjp8,1957
33
- logdetective/server/utils.py,sha256=KiyzzUIVssBc61LhGS0QNC5EY29In3NsG9j58ZRtoNI,4104
31
+ logdetective/server/templates/gitlab_full_comment.md.j2,sha256=4UujUzl3lmdbNEADsxn3HVrjfUiUu2FvUlp9MDFGXQI,2321
32
+ logdetective/server/templates/gitlab_short_comment.md.j2,sha256=2krnMlGqqju2V_6pE0UqUR1P674OFaeX5BMyY5htTOQ,2022
33
+ logdetective/server/utils.py,sha256=0BZ8WmzXNEtkUty1kOyFbBxDZWL0Icc8BUrxuHw9uvs,4015
34
34
  logdetective/skip_snippets.yml,sha256=reGlhPPCo06nNUJWiC2LY-OJOoPdcyOB7QBTSMeh0eg,487
35
- logdetective/utils.py,sha256=_hiRCqFv-CSTTlMKha2uzQ0TX7LZhDcx4ITPqnvGMHo,9718
36
- logdetective-2.7.0.dist-info/METADATA,sha256=v3XvUztZlLeO2_rKC-VP9g1wej2tvROBiRE8wUaAeSM,22598
37
- logdetective-2.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
38
- logdetective-2.7.0.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
39
- logdetective-2.7.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
40
- logdetective-2.7.0.dist-info/RECORD,,
35
+ logdetective/utils.py,sha256=yalhySOF_Gzmqx_Ft9qad3TplAfZ6LOmauGXEJfKWiE,9803
36
+ logdetective-2.9.0.dist-info/METADATA,sha256=cTc0MOpLusXA5aQ7qH_lHFr3IuQMldFiL5Sgc17XIUU,23273
37
+ logdetective-2.9.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
38
+ logdetective-2.9.0.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
39
+ logdetective-2.9.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
40
+ logdetective-2.9.0.dist-info/RECORD,,