t-bug-catcher 0.3.0__tar.gz → 0.4.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 (25) hide show
  1. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/PKG-INFO +2 -1
  2. t_bug_catcher-0.3.0/t_bug_catcher.egg-info/requires.txt → t_bug_catcher-0.4.0/requirements.txt +1 -0
  3. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/setup.cfg +1 -1
  4. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/setup.py +1 -1
  5. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/__init__.py +1 -5
  6. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/bug_catcher.py +8 -1
  7. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/config.py +3 -0
  8. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/jira.py +88 -11
  9. t_bug_catcher-0.4.0/t_bug_catcher/stack_saver.py +186 -0
  10. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/PKG-INFO +2 -1
  11. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
  12. t_bug_catcher-0.3.0/requirements.txt → t_bug_catcher-0.4.0/t_bug_catcher.egg-info/requires.txt +2 -1
  13. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/MANIFEST.in +0 -0
  14. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/README.rst +0 -0
  15. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/pyproject.toml +0 -0
  16. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/bug_snag.py +0 -0
  17. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/exceptions.py +0 -0
  18. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/utils/__init__.py +0 -0
  19. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/utils/common.py +0 -0
  20. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/utils/logger.py +0 -0
  21. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher/workitems.py +0 -0
  22. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
  23. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/not-zip-safe +0 -0
  24. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/t_bug_catcher.egg-info/top_level.txt +0 -0
  25. {t_bug_catcher-0.3.0 → t_bug_catcher-0.4.0}/tests/test_t_bug_catcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: t_bug_catcher
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Bug catcher
5
5
  Home-page: https://www.thoughtful.ai/
6
6
  Author: Thoughtful
@@ -14,6 +14,7 @@ Requires-Python: >=3.9
14
14
  Requires-Dist: requests<3.0.0,>=2.31.0
15
15
  Requires-Dist: bugsnag>=4.6.1
16
16
  Requires-Dist: retry~=0.9.2
17
+ Requires-Dist: whispers>=2.2.1
17
18
 
18
19
  t-bug-catcher
19
20
  ==============
@@ -1,3 +1,4 @@
1
1
  requests<3.0.0,>=2.31.0
2
2
  bugsnag>=4.6.1
3
3
  retry~=0.9.2
4
+ whispers>=2.2.1
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.3.0
2
+ current_version = 0.4.0
3
3
  commit = True
4
4
  tag = False
5
5
 
@@ -26,7 +26,7 @@ setup(
26
26
  packages=find_packages(include=["t_bug_catcher", "t_bug_catcher.*"]),
27
27
  test_suite="tests",
28
28
  url="https://www.thoughtful.ai/",
29
- version="0.3.0",
29
+ version="0.4.0",
30
30
  zip_safe=False,
31
31
  install_requires=install_requirements,
32
32
  )
@@ -3,14 +3,12 @@
3
3
  __author__ = """Thoughtful"""
4
4
  __email__ = "support@thoughtful.ai"
5
5
  # fmt: off
6
- __version__ = '0.3.0'
6
+ __version__ = '0.4.0'
7
7
  # fmt: on
8
8
 
9
9
  from .bug_catcher import (
10
10
  configure,
11
11
  report_error,
12
- report_error_to_jira,
13
- report_error_to_bugsnag,
14
12
  attach_file_to_exception,
15
13
  install_sys_hook,
16
14
  uninstall_sys_hook,
@@ -19,8 +17,6 @@ from .bug_catcher import (
19
17
  __all__ = [
20
18
  "configure",
21
19
  "report_error",
22
- "report_error_to_jira",
23
- "report_error_to_bugsnag",
24
20
  "attach_file_to_exception",
25
21
  "install_sys_hook",
26
22
  "uninstall_sys_hook",
@@ -8,6 +8,7 @@ from typing import List, Optional
8
8
  from .bug_snag import BugSnag
9
9
  from .config import CONFIG
10
10
  from .jira import Jira
11
+ from .stack_saver import StackSaver
11
12
  from .utils import logger
12
13
  from .utils.common import get_frames
13
14
 
@@ -89,6 +90,7 @@ class BugCatcher:
89
90
  self.__bug_snag: BugSnag = BugSnag()
90
91
  self.__configurator: Configurator = Configurator(self.__jira, self.__bug_snag)
91
92
  self.__sys_excepthook = None
93
+ self.__stack_saver = StackSaver()
92
94
 
93
95
  @property
94
96
  def configure(self):
@@ -131,11 +133,14 @@ class BugCatcher:
131
133
  logger.warning(f"Exception {handled_error} already reported.")
132
134
  return
133
135
 
136
+ stack_trace = self.__stack_saver.save_stack_trace(exception)
137
+
134
138
  if self.__configurator.is_jira_configured:
135
139
  self.__jira.report_error(
136
140
  exception=exception,
137
141
  assignee=assignee,
138
142
  attachments=attachments,
143
+ stack_trace=stack_trace,
139
144
  additional_info=description,
140
145
  metadata=metadata,
141
146
  )
@@ -243,8 +248,10 @@ class BugCatcher:
243
248
  logger.warning(f"Exception {handled_error} already reported.")
244
249
  return
245
250
 
251
+ stack_trace = self.__stack_saver.save_stack_trace(exc_value)
252
+
246
253
  if self.__configurator.is_jira_configured:
247
- self.__jira.report_unhandled_error(exc_type, exc_value, exc_traceback)
254
+ self.__jira.report_unhandled_error(exc_type, exc_value, exc_traceback, stack_trace)
248
255
  if self.__configurator.is_bugsnag_configured:
249
256
  self.__bug_snag.report_unhandled_error(exc_type, exc_value, exc_traceback)
250
257
 
@@ -12,6 +12,7 @@ class Config:
12
12
  MAX_ATTACHMENTS: int = 5
13
13
  MAX_ISSUE_ATTACHMENTS: int = 100
14
14
  MAX_DESCRIPTION_LENGTH: int = 250
15
+ SUMMARY_LENGTH: int = 120
15
16
 
16
17
  SUPPORT_BOARD = "AB"
17
18
 
@@ -19,6 +20,7 @@ class Config:
19
20
  f"https://cloud.robocorp.com/organizations/{os.environ.get('RC_ORGANIZATION_ID')}"
20
21
  f"/workspaces/{os.environ.get('RC_WORKSPACE_ID')}/processes"
21
22
  f"/{os.environ.get('RC_PROCESS_ID')}/runs/{os.environ.get('RC_PROCESS_RUN_ID')}/"
23
+ f"stepRuns/{os.environ.get('RC_ACTIVITY_RUN_ID')}/"
22
24
  )
23
25
 
24
26
  ENVIRONMENT = (
@@ -29,6 +31,7 @@ class Config:
29
31
 
30
32
  STAGE = metadata.get("process", dict()).get("implementationStage", "")
31
33
  ADMIN_CODE = metadata.get("process", dict()).get("adminCode", "")
34
+ WORKER_NAME = metadata.get("process", dict()).get("name", "")
32
35
  EMPOWER_URL = metadata.get("process", dict()).get("processRunUrl") or variables.get("processRunUrl")
33
36
 
34
37
 
@@ -7,6 +7,7 @@ import os
7
7
  import re
8
8
  import sys
9
9
  import traceback
10
+ from importlib.metadata import version
10
11
  from pathlib import Path
11
12
  from types import TracebackType
12
13
  from typing import List, Optional, Union
@@ -442,6 +443,34 @@ class Jira:
442
443
  }
443
444
  ]
444
445
 
446
+ @staticmethod
447
+ def __bot_name_markup() -> List[dict]:
448
+ """Create the ai worker markup.
449
+
450
+ Returns:
451
+ dict: The ai worker markup.
452
+ """
453
+ return (
454
+ [
455
+ {
456
+ "type": "paragraph",
457
+ "content": [
458
+ {
459
+ "type": "text",
460
+ "text": "Process name: ",
461
+ "marks": [{"type": "strong"}],
462
+ },
463
+ {
464
+ "type": "text",
465
+ "text": f"{CONFIG.ADMIN_CODE} - {CONFIG.WORKER_NAME}",
466
+ },
467
+ ],
468
+ }
469
+ ]
470
+ if CONFIG.ADMIN_CODE and CONFIG.WORKER_NAME
471
+ else []
472
+ )
473
+
445
474
  @staticmethod
446
475
  def __traceback_markup(exc_traceback_info: str) -> List[dict]:
447
476
  """Create the traceback markup.
@@ -522,6 +551,14 @@ class Jira:
522
551
  {"type": "subsup", "attrs": {"type": "sub"}},
523
552
  ],
524
553
  },
554
+ {
555
+ "type": "text",
556
+ "text": f" (v{version('t_bug_catcher')})",
557
+ "marks": [
558
+ {"type": "em"},
559
+ {"type": "subsup", "attrs": {"type": "sub"}},
560
+ ],
561
+ },
525
562
  ],
526
563
  },
527
564
  ]
@@ -540,7 +577,26 @@ class Jira:
540
577
  },
541
578
  {"type": "underline"},
542
579
  ],
543
- }
580
+ },
581
+ {
582
+ "type": "text",
583
+ "text": " [Robocloud ",
584
+ },
585
+ {
586
+ "type": "text",
587
+ "text": "link",
588
+ "marks": [
589
+ {
590
+ "type": "link",
591
+ "attrs": {"href": CONFIG.RC_RUN_LINK},
592
+ },
593
+ {"type": "underline"},
594
+ ],
595
+ },
596
+ {
597
+ "type": "text",
598
+ "text": "]",
599
+ },
544
600
  ],
545
601
  "robocloud": [
546
602
  {
@@ -601,6 +657,7 @@ class Jira:
601
657
  "type": "doc",
602
658
  "content": []
603
659
  + (self.__error_string_markup(error_string, exc_info) if error_string else [])
660
+ + self.__bot_name_markup()
604
661
  + self.__date_markup()
605
662
  + self.__runlink_markup()
606
663
  + self.__environment_markup()
@@ -902,6 +959,7 @@ class Jira:
902
959
  exception: Optional[Exception] = None,
903
960
  assignee: Optional[str] = None,
904
961
  attachments: Union[List, str, Path, None] = None,
962
+ stack_trace: Optional[str] = None,
905
963
  metadata: Optional[dict] = None,
906
964
  additional_info: Optional[str] = None,
907
965
  ) -> dict:
@@ -911,6 +969,7 @@ class Jira:
911
969
  exception (Exception, optional): The exception to be added to the Jira issue.
912
970
  assignee (str, optional): The assignee to be added to the Jira issue.
913
971
  attachments (List, optional): List of attachments to be added to the Jira issue.
972
+ stack_trace (str, optional): Stack trace to be added to the Jira issue.
914
973
  metadata (dict, optional): Metadata to be added to the Jira issue.
915
974
  additional_info (str, optional): Additional information to be added to the Jira issue.
916
975
 
@@ -955,6 +1014,9 @@ class Jira:
955
1014
  )
956
1015
  return existing_ticket
957
1016
 
1017
+ if stack_trace:
1018
+ attachments.insert(0, stack_trace)
1019
+
958
1020
  assignee_id = None
959
1021
  assignee = assignee if assignee else self._default_assignee
960
1022
  if assignee:
@@ -979,19 +1041,23 @@ class Jira:
979
1041
  attachments=attachments,
980
1042
  labels=["bug_catcher"],
981
1043
  )
1044
+ if os.path.exists(stack_trace):
1045
+ os.remove(stack_trace)
982
1046
  return response
983
1047
  except Exception as ex:
984
1048
  logger.warning(f"Failed to create Jira issue due to: {ex.__class__.__name__}: {ex}")
985
1049
  return False
986
1050
 
987
- def report_unhandled_error(self, exc_type: type, exc_value: Union[Exception, str], exc_traceback: TracebackType):
1051
+ def report_unhandled_error(
1052
+ self, exc_type: type, exc_value: Union[Exception, str], exc_traceback: TracebackType, stack_trace: str = None
1053
+ ):
988
1054
  """Report an unhandled error to Jira.
989
1055
 
990
1056
  Args:
991
1057
  exc_type (type): The type of the exception.
992
1058
  exc_value (Exception, str): The value of the exception.
993
1059
  exc_traceback (TracebackType): The traceback of the exception.
994
- assignee (str, optional): The assignee to be added to the Jira issue.
1060
+ stack_trace (str, optional): Stack trace to be added to the Jira issue.
995
1061
 
996
1062
  Returns:
997
1063
  The response from creating the Jira issue.
@@ -1029,8 +1095,11 @@ class Jira:
1029
1095
  summary=summary,
1030
1096
  description=description,
1031
1097
  assignee_id=assignee_id,
1098
+ attachments=[stack_trace] if stack_trace else None,
1032
1099
  labels=["bug_catcher", "fatal_error"],
1033
1100
  )
1101
+ if os.path.exists(stack_trace):
1102
+ os.remove(stack_trace)
1034
1103
  return response
1035
1104
  except Exception as ex:
1036
1105
  logger.warning(f"Failed to create Jira issue due to: {ex.__class__.__name__}: {ex}")
@@ -1204,7 +1273,7 @@ class Jira:
1204
1273
  return response[0]["accountId"]
1205
1274
 
1206
1275
  @staticmethod
1207
- def remove_locators_from_exception(exception: Union[Exception, str]) -> str:
1276
+ def sanitize_summary(exception: Union[Exception, str]) -> str:
1208
1277
  """Remove locators from the exception.
1209
1278
 
1210
1279
  Args:
@@ -1213,9 +1282,11 @@ class Jira:
1213
1282
  Returns:
1214
1283
  str: The cleaned exception string.
1215
1284
  """
1285
+ message = re.sub("<([a-z]+)(?![^>]*\/>)[^>]*>", r"<\1>", str(exception))
1286
+ message = re.sub(">([^<]+)<\/", ">...</", message)
1216
1287
  if "selenium" not in exception.__class__.__name__.lower() and not isinstance(exception, AssertionError):
1217
- return str(exception)
1218
- return re.sub(r"\'(.+)\'", "'...'", str(exception))
1288
+ return str(message)
1289
+ return re.sub(r"\'(.+)\'", "'...'", message)
1219
1290
 
1220
1291
  def __create_summary(self, exc_type: type, exc_value: Union[Exception, str], exc_traceback: TracebackType) -> str:
1221
1292
  """Create the summary of the ticket.
@@ -1230,10 +1301,16 @@ class Jira:
1230
1301
  """
1231
1302
  frames = get_frames(exc_traceback)
1232
1303
  file_name, line_no, _, _ = frames[-1]
1233
- summary = (
1234
- f"[{exc_type.__name__}:{os.path.basename(file_name)}:{line_no}] "
1235
- f"{self.remove_locators_from_exception(exc_value)}"
1236
- )
1304
+ summary = f"[{exc_type.__name__}:{os.path.basename(file_name)}:{line_no}]"
1237
1305
  if self._project_key == CONFIG.SUPPORT_BOARD and CONFIG.ADMIN_CODE:
1238
1306
  summary = CONFIG.ADMIN_CODE + " - " + summary
1239
- return summary
1307
+ if CONFIG.LIMITS.SUMMARY_LENGTH <= len(summary):
1308
+ return summary
1309
+ else:
1310
+ message = self.sanitize_summary(exc_value)
1311
+ message = (
1312
+ message
1313
+ if len(message) <= CONFIG.LIMITS.SUMMARY_LENGTH - len(summary)
1314
+ else message[: CONFIG.LIMITS.SUMMARY_LENGTH - len(summary)] + "..."
1315
+ )
1316
+ return summary + " " + message
@@ -0,0 +1,186 @@
1
+ import inspect
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ from datetime import datetime
7
+ from json import JSONEncoder
8
+ from pathlib import Path
9
+ from types import FunctionType, ModuleType
10
+ from typing import Optional
11
+
12
+ import whispers
13
+
14
+ from .utils import logger
15
+
16
+
17
+ class _Encoder(JSONEncoder):
18
+ """Encoder class for encoding the Episode object to json."""
19
+
20
+ def default(self, o):
21
+ """This method is used to encode the Episode object to json.
22
+
23
+ Args:
24
+ o (object): The object to be encoded.
25
+
26
+ Returns:
27
+ str: The json string.
28
+ """
29
+ if hasattr(o, "__dict__"):
30
+ return o.__dict__
31
+ if isinstance(o, datetime):
32
+ return o.isoformat()
33
+ if isinstance(o, Path):
34
+ return str(o)
35
+ return JSONEncoder.default(self, o)
36
+
37
+
38
+ class StackSaver:
39
+ """A class to save the stack trace."""
40
+
41
+ def __init__(self):
42
+ """Initializes the StackSaver class."""
43
+ pass
44
+
45
+ @staticmethod
46
+ def strip_path(path: str):
47
+ """A static method to strip the current working directory path from the input.
48
+
49
+ Args:
50
+ path (str): The path from which to strip the current working directory path.
51
+
52
+ Returns:
53
+ str: The stripped path.
54
+ """
55
+ return path.replace(os.getcwd(), "").strip(os.sep)
56
+
57
+ @staticmethod
58
+ def serialize_frame_info(frame_info: dict) -> dict:
59
+ """A static method to serialize the frame info.
60
+
61
+ Args:
62
+ frame_info (dict): The frame info to be serialized.
63
+
64
+ Returns:
65
+ dict: The serialized frame info.
66
+ """
67
+ run_locals = {}
68
+ run_args = {}
69
+ if frame_info["locals"]:
70
+ for key, value in frame_info["locals"].items():
71
+ if isinstance(value, dict):
72
+ run_locals[str(key)] = value
73
+ else:
74
+ run_locals[str(key)] = str(value)
75
+ for key, value in frame_info["args"].items():
76
+ if isinstance(value, dict):
77
+ run_args[str(key)] = value
78
+ else:
79
+ run_args[str(key)] = str(value)
80
+ serializable_frame_info = {
81
+ "filename": frame_info["filename"],
82
+ "function_name": frame_info["function_name"],
83
+ "locals": run_locals,
84
+ "args": run_args,
85
+ }
86
+ return serializable_frame_info
87
+
88
+ @staticmethod
89
+ def filter_variables(variables: dict) -> dict:
90
+ """A static method to filter the variables.
91
+
92
+ Args:
93
+ variables (dict): The variables to be filtered.
94
+
95
+ Returns:
96
+ dict: The filtered variables.
97
+ """
98
+ if not isinstance(variables, dict):
99
+ return variables
100
+ else:
101
+ local_variables = {}
102
+ for var_name, var in variables.items():
103
+ if re.match(r"^__\w+__$", var_name):
104
+ continue
105
+ if isinstance(var, (ModuleType, FunctionType)):
106
+ continue
107
+ local_variables[var_name] = var
108
+ return local_variables
109
+
110
+ def mask_credentials(self, file_path: str) -> None:
111
+ """A method to mask the credentials in the file.
112
+
113
+ Args:
114
+ file_path (str): The path of the file to be masked.
115
+
116
+ Raises:
117
+ Exception: If the masking fails.
118
+
119
+ Returns:
120
+ None
121
+ """
122
+ with open(file_path, "r") as f:
123
+ filedata = f.readlines()
124
+
125
+ secrets = [secret for secret in whispers.secrets(file_path)]
126
+
127
+ for index, line in enumerate(filedata):
128
+ if not secrets:
129
+ break
130
+
131
+ for secret in secrets:
132
+ if secret.key in line and secret.value in line:
133
+ filedata[index] = line.replace(secret.value, secret.value[:1] + "***")
134
+ secrets.pop(secrets.index(secret))
135
+ break
136
+
137
+ if secrets:
138
+ logger.warning("Failed to mask credentials")
139
+ os.remove(file_path)
140
+ raise Exception("Failed to mask credentials")
141
+
142
+ with open(file_path, "w") as file:
143
+ file.writelines(filedata)
144
+
145
+ def save_stack_trace(self, exception: Optional[Exception] = None):
146
+ """A method to save the stack trace.
147
+
148
+ Args:
149
+ exception (Exception, optional): The exception to be saved. Defaults to None.
150
+
151
+ Returns:
152
+ Optional[str]: The path of the saved stack trace.
153
+ """
154
+ try:
155
+ frames = []
156
+ stack_details_json = []
157
+ tb = exception.__traceback__ if exception else sys.exc_info()[2]
158
+ while tb is not None:
159
+ frame = tb.tb_frame
160
+ if "site-packages" in frame.f_code.co_filename:
161
+ tb = tb.tb_next
162
+ continue
163
+ frames.append(frame)
164
+ tb = tb.tb_next
165
+ frames = frames[:3]
166
+
167
+ for frame in frames:
168
+ frame_info = {
169
+ "filename": self.strip_path(frame.f_code.co_filename),
170
+ "function_name": frame.f_code.co_name,
171
+ "locals": self.filter_variables(frame.f_locals),
172
+ "args": self.filter_variables(inspect.getargvalues(frame)[3]),
173
+ }
174
+ stack_details_json.append(self.serialize_frame_info(frame_info))
175
+
176
+ file_path = f"stack_details_{datetime.now().strftime('%Y%m%d%H%M%S')}.json"
177
+
178
+ with open(file_path, "w") as f:
179
+ json.dump(stack_details_json, f, indent=4, cls=_Encoder)
180
+
181
+ self.mask_credentials(file_path)
182
+
183
+ return file_path
184
+ except Exception as e:
185
+ logger.warning(f"Failed to save stack trace: {e}")
186
+ return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: t_bug_catcher
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Bug catcher
5
5
  Home-page: https://www.thoughtful.ai/
6
6
  Author: Thoughtful
@@ -14,6 +14,7 @@ Requires-Python: >=3.9
14
14
  Requires-Dist: requests<3.0.0,>=2.31.0
15
15
  Requires-Dist: bugsnag>=4.6.1
16
16
  Requires-Dist: retry~=0.9.2
17
+ Requires-Dist: whispers>=2.2.1
17
18
 
18
19
  t-bug-catcher
19
20
  ==============
@@ -10,6 +10,7 @@ t_bug_catcher/bug_snag.py
10
10
  t_bug_catcher/config.py
11
11
  t_bug_catcher/exceptions.py
12
12
  t_bug_catcher/jira.py
13
+ t_bug_catcher/stack_saver.py
13
14
  t_bug_catcher/workitems.py
14
15
  t_bug_catcher.egg-info/PKG-INFO
15
16
  t_bug_catcher.egg-info/SOURCES.txt
@@ -1,3 +1,4 @@
1
1
  requests<3.0.0,>=2.31.0
2
2
  bugsnag>=4.6.1
3
- retry~=0.9.2
3
+ retry~=0.9.2
4
+ whispers>=2.2.1
File without changes
File without changes