t-bug-catcher 0.6.13__tar.gz → 1.1.1.2766__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 (36) hide show
  1. t_bug_catcher-1.1.1.2766/MANIFEST.in +1 -0
  2. t_bug_catcher-1.1.1.2766/PKG-INFO +9 -0
  3. t_bug_catcher-1.1.1.2766/backend_shim.py +31 -0
  4. t_bug_catcher-1.1.1.2766/setup.cfg +4 -0
  5. t_bug_catcher-1.1.1.2766/setup.py +27 -0
  6. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/__init__.py +4 -9
  7. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/bug_catcher.py +55 -37
  8. t_bug_catcher-1.1.1.2766/t_bug_catcher/bug_snag.py +136 -0
  9. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/config.py +6 -4
  10. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/jira.py +334 -162
  11. t_bug_catcher-1.1.1.2766/t_bug_catcher/py.typed +0 -0
  12. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/stack_saver.py +26 -14
  13. t_bug_catcher-1.1.1.2766/t_bug_catcher/utils/__init__.py +5 -0
  14. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/utils/common.py +58 -33
  15. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/utils/logger.py +2 -0
  16. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/workitems.py +2 -2
  17. t_bug_catcher-1.1.1.2766/t_bug_catcher.egg-info/PKG-INFO +9 -0
  18. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher.egg-info/SOURCES.txt +4 -8
  19. t_bug_catcher-1.1.1.2766/t_bug_catcher.egg-info/requires.txt +3 -0
  20. t_bug_catcher-0.6.13/MANIFEST.in +0 -1
  21. t_bug_catcher-0.6.13/PKG-INFO +0 -70
  22. t_bug_catcher-0.6.13/README.rst +0 -44
  23. t_bug_catcher-0.6.13/pyproject.toml +0 -7
  24. t_bug_catcher-0.6.13/requirements.txt +0 -3
  25. t_bug_catcher-0.6.13/setup.cfg +0 -17
  26. t_bug_catcher-0.6.13/setup.py +0 -34
  27. t_bug_catcher-0.6.13/t_bug_catcher/bug_snag.py +0 -90
  28. t_bug_catcher-0.6.13/t_bug_catcher/resources/whispers_config.yml +0 -44
  29. t_bug_catcher-0.6.13/t_bug_catcher/utils/__init__.py +0 -2
  30. t_bug_catcher-0.6.13/t_bug_catcher.egg-info/PKG-INFO +0 -70
  31. t_bug_catcher-0.6.13/t_bug_catcher.egg-info/requires.txt +0 -3
  32. t_bug_catcher-0.6.13/tests/test_t_bug_catcher.py +0 -93
  33. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher/exceptions.py +0 -0
  34. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
  35. /t_bug_catcher-0.6.13/t_bug_catcher.egg-info/not-zip-safe → /t_bug_catcher-1.1.1.2766/t_bug_catcher.egg-info/namespace_packages.txt +0 -0
  36. {t_bug_catcher-0.6.13 → t_bug_catcher-1.1.1.2766}/t_bug_catcher.egg-info/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ include *.py
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: t-bug-catcher
3
+ Version: 1.1.1.2766
4
+ Requires-Python: >=3.12,<3.14
5
+ Requires-Dist: bugsnag==4.8.0
6
+ Requires-Dist: requests==2.32.5
7
+ Requires-Dist: retry==0.9.2
8
+ Dynamic: requires-dist
9
+ Dynamic: requires-python
@@ -0,0 +1,31 @@
1
+
2
+ # DO NOT EDIT THIS FILE -- AUTOGENERATED BY PANTS
3
+
4
+ import errno
5
+ import os
6
+ import setuptools.build_meta
7
+
8
+ backend = setuptools.build_meta.__legacy__
9
+
10
+ dist_dir = "dist/"
11
+ build_wheel = True
12
+ build_sdist = True
13
+ wheel_config_settings = {
14
+ }
15
+ sdist_config_settings = {
16
+ }
17
+
18
+ # Python 2.7 doesn't have the exist_ok arg on os.makedirs().
19
+ try:
20
+ os.makedirs(dist_dir)
21
+ except OSError as e:
22
+ if e.errno != errno.EEXIST:
23
+ raise
24
+
25
+ wheel_path = backend.build_wheel(dist_dir, wheel_config_settings) if build_wheel else None
26
+ sdist_path = backend.build_sdist(dist_dir, sdist_config_settings) if build_sdist else None
27
+
28
+ if wheel_path:
29
+ print("wheel: {wheel_path}".format(wheel_path=wheel_path))
30
+ if sdist_path:
31
+ print("sdist: {sdist_path}".format(sdist_path=sdist_path))
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+
2
+ # DO NOT EDIT THIS FILE -- AUTOGENERATED BY PANTS
3
+ # Target: libraries/t_bug_catcher:t-bug-catcher
4
+
5
+ from setuptools import setup
6
+
7
+ setup(**{
8
+ 'install_requires': (
9
+ 'bugsnag==4.8.0',
10
+ 'requests==2.32.5',
11
+ 'retry==0.9.2',
12
+ ),
13
+ 'name': 't-bug-catcher',
14
+ 'namespace_packages': (
15
+ ),
16
+ 'package_data': {
17
+ 't_bug_catcher': (
18
+ 'py.typed',
19
+ ),
20
+ },
21
+ 'packages': (
22
+ 't_bug_catcher',
23
+ 't_bug_catcher.utils',
24
+ ),
25
+ 'python_requires': '>=3.12,<3.14',
26
+ 'version': '1.1.1.2766',
27
+ })
@@ -1,20 +1,15 @@
1
1
  """Top-level package for t-bug-catcher."""
2
2
 
3
- __author__ = """Thoughtful"""
4
- __email__ = "support@thoughtful.ai"
5
- # fmt: off
6
- __version__ = '0.6.13'
7
- # fmt: on
8
-
9
3
  from .bug_catcher import (
10
- configure,
11
- report_error,
12
4
  attach_file_to_exception,
5
+ configure,
6
+ get_errors_count,
13
7
  install_sys_hook,
8
+ report_error,
14
9
  uninstall_sys_hook,
15
- get_errors_count,
16
10
  )
17
11
 
12
+
18
13
  __all__ = [
19
14
  "configure",
20
15
  "report_error",
@@ -1,10 +1,10 @@
1
1
  """JiraPoster class for interacting with the Jira API."""
2
2
 
3
3
  import inspect
4
- import os
5
4
  import sys
5
+
6
+ from pathlib import Path
6
7
  from types import TracebackType
7
- from typing import List, Optional
8
8
 
9
9
  from .bug_snag import BugSnag
10
10
  from .config import CONFIG
@@ -29,10 +29,10 @@ class Configurator:
29
29
  login: str,
30
30
  api_token: str,
31
31
  project_key: str,
32
- webhook_url: Optional[str] = None,
33
- webhook_secret: Optional[str] = None,
34
- default_assignee: Optional[str] = None,
35
- ):
32
+ webhook_url: str | None = None,
33
+ webhook_secret: str | None = None,
34
+ default_assignee: str | None = None,
35
+ ) -> None:
36
36
  """Configures the JiraPoster and BugSnag classes.
37
37
 
38
38
  Args:
@@ -55,7 +55,7 @@ class Configurator:
55
55
  default_assignee=default_assignee,
56
56
  )
57
57
 
58
- def bugsnag(self, api_key: str):
58
+ def bugsnag(self, api_key: str) -> None:
59
59
  """Configures the BugSnag class.
60
60
 
61
61
  Args:
@@ -67,12 +67,12 @@ class Configurator:
67
67
  self.__bug_snag_configured = self.__bug_snag.config(api_key=api_key)
68
68
 
69
69
  @property
70
- def is_jira_configured(self):
70
+ def is_jira_configured(self) -> bool:
71
71
  """Checks if the JiraPoster class has been configured."""
72
72
  return self.__jira_configured
73
73
 
74
74
  @property
75
- def is_bugsnag_configured(self):
75
+ def is_bugsnag_configured(self) -> bool:
76
76
  """Checks if the BugSnag class has been configured."""
77
77
  return self.__bug_snag_configured
78
78
 
@@ -98,24 +98,24 @@ class BugCatcher:
98
98
  self.__errors_count = 0
99
99
 
100
100
  @property
101
- def configure(self):
101
+ def configure(self) -> Configurator:
102
102
  """Configures the JiraPoster and BugSnag classes."""
103
103
  return self.__configurator
104
104
 
105
- def get_errors_count(self):
105
+ def get_errors_count(self) -> int:
106
106
  """Returns the number of exceptions reported."""
107
107
  return self.__errors_count
108
108
 
109
109
  def report_error(
110
110
  self,
111
- exception: Optional[Exception] = None,
112
- description: Optional[str] = None,
113
- metadata: Optional[dict] = None,
114
- attachments: Optional[List] = None,
115
- assignee: Optional[str] = None,
116
- team: Optional[str] = None,
117
- group_by: Optional[str] = None,
118
- ):
111
+ exception: Exception | None = None,
112
+ description: str | None = None,
113
+ metadata: dict | None = None,
114
+ attachments: list | None = None,
115
+ assignee: str | None = None,
116
+ team: str | None = None,
117
+ group_by: str | None = None,
118
+ ) -> None:
119
119
  """Reports an error to the Jira project.
120
120
 
121
121
  Args:
@@ -134,8 +134,13 @@ class BugCatcher:
134
134
  logger.warning("Reporting an error is not supported in local environment.")
135
135
  return
136
136
 
137
- if not self.__configurator.is_jira_configured and not self.__configurator.is_bugsnag_configured:
138
- logger.warning("Jira and BugSnag are not configured. Please configure them before reporting an error.")
137
+ if (
138
+ not self.__configurator.is_jira_configured
139
+ and not self.__configurator.is_bugsnag_configured
140
+ ):
141
+ logger.warning(
142
+ "Jira and BugSnag are not configured. Please configure them before reporting an error."
143
+ )
139
144
  return
140
145
 
141
146
  if not exception:
@@ -197,19 +202,21 @@ class BugCatcher:
197
202
  )
198
203
 
199
204
  frames = get_frames(exception.__traceback__)
200
- exc_info = f"{os.path.basename(frames[-1].filename)}:{frames[-1].name}:{frames[-1].lineno}"
205
+ exc_info = (
206
+ f"{Path(frames[-1].filename).name}:{frames[-1].name}:{frames[-1].lineno}"
207
+ )
201
208
  exception.handled_error = exc_info
202
209
  logger.info(f"Exception {exc_info} reported.")
203
210
  self.__errors_count += 1
204
211
 
205
212
  def report_error_to_jira(
206
213
  self,
207
- exception: Optional[Exception] = None,
208
- assignee: Optional[str] = None,
209
- attachments: Optional[List] = None,
210
- metadata: Optional[dict] = None,
211
- additional_info: Optional[str] = None,
212
- ):
214
+ exception: Exception | None = None,
215
+ assignee: str | None = None,
216
+ attachments: list | None = None,
217
+ metadata: dict | None = None,
218
+ additional_info: str | None = None,
219
+ ) -> None:
213
220
  """Creates a Jira issue with the given attachments.
214
221
 
215
222
  Args:
@@ -235,7 +242,9 @@ class BugCatcher:
235
242
  metadata=metadata,
236
243
  )
237
244
 
238
- def report_error_to_bugsnag(self, exception: Optional[Exception] = None, metadata: Optional[dict] = None):
245
+ def report_error_to_bugsnag(
246
+ self, exception: Exception | None = None, metadata: dict | None = None
247
+ ) -> None:
239
248
  """Sends an error to BugSnag.
240
249
 
241
250
  Args:
@@ -271,7 +280,9 @@ class BugCatcher:
271
280
  else:
272
281
  exception.custom_attachments = [attachment]
273
282
 
274
- def __excepthook(self, exc_type: type, exc_value: Exception, exc_traceback: TracebackType) -> None:
283
+ def __excepthook(
284
+ self, exc_type: type, exc_value: Exception, exc_traceback: TracebackType
285
+ ) -> None:
275
286
  """Handles unhandled exceptions.
276
287
 
277
288
  Args:
@@ -286,8 +297,13 @@ class BugCatcher:
286
297
  logger.warning("Reporting an error is not supported in local environment.")
287
298
  return
288
299
 
289
- if not self.__configurator.is_jira_configured and not self.__configurator.is_bugsnag_configured:
290
- logger.warning("Jira and BugSnag are not configured. Please configure them before reporting an error.")
300
+ if (
301
+ not self.__configurator.is_jira_configured
302
+ and not self.__configurator.is_bugsnag_configured
303
+ ):
304
+ logger.warning(
305
+ "Jira and BugSnag are not configured. Please configure them before reporting an error."
306
+ )
291
307
  return
292
308
 
293
309
  handled_error = getattr(exc_value, "handled_error", None)
@@ -302,11 +318,13 @@ class BugCatcher:
302
318
  if self.__configurator.is_bugsnag_configured:
303
319
  self.__bug_snag.report_unhandled_error(exc_type, exc_value, exc_traceback)
304
320
  frames = get_frames(exc_value.__traceback__)
305
- exc_info = f"{os.path.basename(frames[-1].filename)}:{frames[-1].name}:{frames[-1].lineno}"
321
+ exc_info = (
322
+ f"{Path(frames[-1].filename).name}:{frames[-1].name}:{frames[-1].lineno}"
323
+ )
306
324
  logger.info(f"Exception {exc_info} reported.")
307
325
  self.__errors_count += 1
308
326
 
309
- def __get_sys_hook_attribute(self, attribute: str = "bug_catcher_client"):
327
+ def __get_sys_hook_attribute(self, attribute: str = "bug_catcher_client") -> None:
310
328
  """Checks if the system hook is installed.
311
329
 
312
330
  Args:
@@ -317,14 +335,14 @@ class BugCatcher:
317
335
  """
318
336
  return getattr(sys.excepthook, attribute, None)
319
337
 
320
- def install_sys_hook(self):
338
+ def install_sys_hook(self) -> None:
321
339
  """Installs a system hook to handle unhandled exceptions."""
322
340
  if self.__get_sys_hook_attribute():
323
341
  return
324
342
 
325
343
  self.__sys_excepthook = sys.excepthook
326
344
 
327
- def excepthook(*exc_info):
345
+ def excepthook(*exc_info) -> None:
328
346
  self.__excepthook(*exc_info)
329
347
 
330
348
  if self.__sys_excepthook:
@@ -333,7 +351,7 @@ class BugCatcher:
333
351
  sys.excepthook = excepthook
334
352
  sys.excepthook.bug_catcher_client = self
335
353
 
336
- def uninstall_sys_hook(self):
354
+ def uninstall_sys_hook(self) -> None:
337
355
  """Uninstalls the system hook to handle unhandled exceptions."""
338
356
  client = self.__get_sys_hook_attribute()
339
357
 
@@ -0,0 +1,136 @@
1
+ import os
2
+
3
+ import bugsnag
4
+ import requests
5
+
6
+ from .config import CONFIG
7
+ from .utils import logger
8
+ from .workitems import variables
9
+
10
+
11
+ bugsnag.configuration.auto_notify = False
12
+
13
+ # Default error message for BugSnag reports
14
+ _DEFAULT_ERROR_MESSAGE = "An error occurred during execution"
15
+
16
+
17
+ class BugSnag:
18
+ """BugSnag class for interacting with the BugSnag API."""
19
+
20
+ def __init__(self):
21
+ """Initializes the BugSnag class."""
22
+ pass
23
+
24
+ @staticmethod
25
+ def _create_generic_error(exception_type: type) -> Exception:
26
+ """Creates a generic error of the given type with fallback handling.
27
+
28
+ Attempts to create an exception of the specified type with the default message.
29
+ If that fails, tries to create an empty exception of the same type.
30
+ If both fail, falls back to a generic Exception with the default message.
31
+
32
+ Args:
33
+ exception_type (type): The type of exception to create.
34
+
35
+ Returns:
36
+ Exception: An exception instance of the requested type (or Exception as fallback).
37
+ """
38
+ # Try to create an exception of the same type with the default message
39
+ # If that fails, try with no arguments (empty exception)
40
+ # If both fail, fall back to using Exception
41
+ try:
42
+ error = exception_type(_DEFAULT_ERROR_MESSAGE)
43
+ except (TypeError, ValueError):
44
+ try:
45
+ # Try instantiating with no arguments (empty exception of same type)
46
+ error = exception_type()
47
+ except (TypeError, ValueError):
48
+ # Fall back to Exception if the custom exception type can't be instantiated
49
+ error = Exception(_DEFAULT_ERROR_MESSAGE)
50
+ return error
51
+
52
+ def config(self, api_key: str) -> bool:
53
+ """Configures the BugSnag class.
54
+
55
+ Args:
56
+ api_key (str): The API key for the BugSnag account.
57
+
58
+ Returns:
59
+ bool: True if the configuration was successful, False otherwise.
60
+ """
61
+ try:
62
+ bugsnag.configure(
63
+ api_key=api_key, release_stage=CONFIG.ENVIRONMENT, auto_notify=False
64
+ )
65
+ run_id = os.environ.get("RC_PROCESS_RUN_ID", "unknown")
66
+ bugsnag.add_metadata_tab(
67
+ "Metadata",
68
+ {
69
+ "run_url": variables.get("processRunUrl", ""),
70
+ "run_by": variables.get("userEmail", ""),
71
+ "run_id": run_id,
72
+ },
73
+ )
74
+ response = requests.request(
75
+ "POST",
76
+ "https://otlp.bugsnag.com/v1/traces",
77
+ headers={
78
+ "Content-Type": "application/json",
79
+ "Bugsnag-Api-Key": api_key,
80
+ "Bugsnag-Payload-Version": "4",
81
+ "Bugsnag-Span-Sampling": "True",
82
+ },
83
+ data='{"message": "test"}',
84
+ timeout=None, # noqa: S113
85
+ )
86
+ if response.status_code not in [200, 201, 202, 204]:
87
+ logger.warning(f"Error connecting to Bugsnag: {response.text}")
88
+ return False
89
+ return True
90
+ except (requests.RequestException, ValueError, KeyError) as ex:
91
+ logger.warning(f"Failed to configure Bugsnag: {ex}")
92
+ return False
93
+
94
+ def report_error(
95
+ self, exception: Exception | None = None, metadata: dict | None = None
96
+ ) -> None:
97
+ """Sends an error to BugSnag.
98
+
99
+ Args:
100
+ exception (Exception, optional): The exception to report.
101
+ metadata (dict, optional): The metadata to be added to the Bugsnag issue. Defaults to None.
102
+
103
+ Returns:
104
+ None
105
+ """
106
+ run_id = os.environ.get("RC_PROCESS_RUN_ID", "unknown")
107
+ # Use the same exception type as provided, or default to Exception
108
+ exception_type = type(exception) if exception is not None else Exception
109
+ error = self._create_generic_error(exception_type)
110
+
111
+ bugsnag.notify(
112
+ error,
113
+ metadata={"run_id": run_id},
114
+ )
115
+
116
+ def report_unhandled_error(self, exc_type, exc_value, traceback) -> None:
117
+ """Sends an unhandled exception to BugSnag.
118
+
119
+ Args:
120
+ exc_type (type): The type of the exception.
121
+ exc_value (Exception): The value of the exception.
122
+ traceback (traceback): The traceback of the exception.
123
+
124
+ Returns:
125
+ None
126
+ """
127
+ run_id = os.environ.get("RC_PROCESS_RUN_ID", "unknown")
128
+ # Use the same exception type as provided, or default to Exception
129
+ exception_type = exc_type if exc_type is not None else Exception
130
+ error = self._create_generic_error(exception_type)
131
+
132
+ bugsnag.notify(
133
+ error,
134
+ metadata={"run_id": run_id},
135
+ severity="error",
136
+ )
@@ -64,10 +64,12 @@ class Config:
64
64
  else variables.get("environment", "local")
65
65
  )
66
66
 
67
- STAGE = metadata.get("process", dict()).get("implementationStage", "")
68
- ADMIN_CODE = metadata.get("process", dict()).get("adminCode", "")
69
- WORKER_NAME = metadata.get("process", dict()).get("name", "")
70
- EMPOWER_URL = metadata.get("process", dict()).get("processRunUrl") or variables.get("processRunUrl")
67
+ STAGE = metadata.get("process", {}).get("implementationStage", "")
68
+ ADMIN_CODE = metadata.get("process", {}).get("adminCode", "")
69
+ WORKER_NAME = metadata.get("process", {}).get("name", "")
70
+ EMPOWER_URL = metadata.get("process", {}).get("processRunUrl") or variables.get(
71
+ "processRunUrl"
72
+ )
71
73
 
72
74
 
73
75
  CONFIG = Config()