understand-py 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Understand
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: understand-py
3
+ Version: 0.1.0
4
+ Summary: Framework-neutral Python SDK for sending logs and exceptions to Understand.io.
5
+ Author: Understand
6
+ License-Expression: MIT
7
+ Keywords: understand.io,logging,errors,exceptions
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # Understand Python
24
+
25
+ Framework-neutral Python SDK for sending logs and exceptions to Understand.io.
26
+
27
+ This package is for Python applications. It does not run inside Laravel and it
28
+ does not reimplement the browser JavaScript SDK. Where it shares configuration
29
+ names with the Laravel package, that is config parity only.
30
+
31
+ ## Installation
32
+
33
+ Install from PyPI:
34
+
35
+ ```bash
36
+ python3 -m pip install understand-py
37
+ ```
38
+
39
+ Or add it to `requirements.txt`:
40
+
41
+ ```text
42
+ understand-py
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Set your Understand.io input token:
48
+
49
+ ```bash
50
+ export UNDERSTAND_TOKEN="your-input-token"
51
+ export UNDERSTAND_ENV="production"
52
+ ```
53
+
54
+ Or configure it directly in code:
55
+
56
+ ```python
57
+ from understand import UnderstandConfig
58
+
59
+ config = UnderstandConfig(
60
+ token="your-input-token",
61
+ environment="production",
62
+ )
63
+ ```
64
+
65
+ ## Basic Usage
66
+
67
+ Send a log message:
68
+
69
+ ```python
70
+ from understand import UnderstandConfig, UnderstandLogger
71
+
72
+ logger = UnderstandLogger.from_config(
73
+ UnderstandConfig(token="your-input-token", environment="production")
74
+ )
75
+
76
+ logger.error("Understand.io test error", context={"source": "my-app"})
77
+ ```
78
+
79
+ Capture and send an exception:
80
+
81
+ ```python
82
+ from understand import UnderstandConfig, UnderstandLogger
83
+
84
+ logger = UnderstandLogger.from_config(
85
+ UnderstandConfig(token="your-input-token", environment="production")
86
+ )
87
+
88
+ try:
89
+ raise RuntimeError("Payment capture failed")
90
+ except RuntimeError as exc:
91
+ logger.exception(exc, context={"feature": "payments"})
92
+ ```
93
+
94
+ ## Stdlib Logging
95
+
96
+ Attach Understand to Python's built-in `logging` package:
97
+
98
+ ```python
99
+ import logging
100
+
101
+ from understand import UnderstandConfig, UnderstandLogger
102
+ from understand.integrations.logging import UnderstandLoggingHandler
103
+
104
+ understand_logger = UnderstandLogger.from_config(
105
+ UnderstandConfig.from_env()
106
+ )
107
+
108
+ logging.getLogger().addHandler(UnderstandLoggingHandler(understand_logger))
109
+ logging.getLogger().setLevel(logging.INFO)
110
+
111
+ logging.error("This error is sent to Understand.io")
112
+ ```
113
+
114
+ Exceptions logged with `exc_info=True` are sent as structured exception events:
115
+
116
+ ```python
117
+ try:
118
+ raise ValueError("Invalid checkout amount")
119
+ except ValueError:
120
+ logging.exception("Checkout failed")
121
+ ```
122
+
123
+ ## Request Context
124
+
125
+ For web applications, attach request-like metadata before logging:
126
+
127
+ ```python
128
+ from understand import RequestContext, UnderstandConfig, UnderstandLogger
129
+
130
+ logger = UnderstandLogger.from_config(
131
+ UnderstandConfig(token="your-input-token", environment="production")
132
+ )
133
+
134
+ logger.field_provider.set_request_context(
135
+ RequestContext(
136
+ path="/checkout/payments",
137
+ method="POST",
138
+ route_name="payments.create",
139
+ query={"debug": "1"},
140
+ body={"amount": 4999, "password": "hidden automatically"},
141
+ client_ip="203.0.113.10",
142
+ user_agent="Mozilla/5.0",
143
+ user_id="user-123",
144
+ session_id="session-id",
145
+ )
146
+ )
147
+ ```
148
+
149
+ Fields like `password`, `password_confirmation`, `access_token`, `secret_key`,
150
+ `token`, and `access_key` are replaced with `[value hidden]` by default.
151
+
152
+ Environment variables:
153
+
154
+ - `UNDERSTAND_TOKEN`
155
+ - `UNDERSTAND_ENABLED`
156
+ - `UNDERSTAND_API_URL`
157
+ - `UNDERSTAND_HANDLER`
158
+ - `UNDERSTAND_ENV`
159
+ - `UNDERSTAND_SQL`
160
+ - `UNDERSTAND_SQL_BINDINGS`
161
+ - `UNDERSTAND_QUERY_STRING`
162
+ - `UNDERSTAND_REQUEST_BODY`
163
+ - `UNDERSTAND_POST_DATA` as a SDK config-parity alias for request body collection
164
+ - `UNDERSTAND_HIDDEN_REQUEST_FIELDS`
165
+ - `UNDERSTAND_SSL_CA_BUNDLE`
166
+
167
+ For browser JavaScript error tracking, use the separate `understand-js` SDK.
168
+
169
+ ## Long-Running Workers
170
+
171
+ For long-running workers, call `reset_context()` between jobs to regenerate the
172
+ request token and clear collected data:
173
+
174
+ ```python
175
+ for job in jobs:
176
+ logger.reset_context()
177
+
178
+ try:
179
+ process_job(job)
180
+ except Exception as exc:
181
+ logger.exception(exc, context={"job_id": job.id})
182
+ ```
183
+
184
+ ## Local Testing Without Sending Data
185
+
186
+ Use `CallbackHandler` to inspect payloads without making network requests:
187
+
188
+ ```python
189
+ import json
190
+
191
+ from understand import CallbackHandler, UnderstandConfig, UnderstandLogger
192
+
193
+ events = []
194
+ logger = UnderstandLogger.from_config(
195
+ UnderstandConfig(token="test-token", environment="local"),
196
+ handler=CallbackHandler(lambda payload: events.append(json.loads(payload))),
197
+ )
198
+
199
+ logger.error("Local test error")
200
+
201
+ print(json.dumps(events, indent=2))
202
+ ```
@@ -0,0 +1,180 @@
1
+ # Understand Python
2
+
3
+ Framework-neutral Python SDK for sending logs and exceptions to Understand.io.
4
+
5
+ This package is for Python applications. It does not run inside Laravel and it
6
+ does not reimplement the browser JavaScript SDK. Where it shares configuration
7
+ names with the Laravel package, that is config parity only.
8
+
9
+ ## Installation
10
+
11
+ Install from PyPI:
12
+
13
+ ```bash
14
+ python3 -m pip install understand-py
15
+ ```
16
+
17
+ Or add it to `requirements.txt`:
18
+
19
+ ```text
20
+ understand-py
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ Set your Understand.io input token:
26
+
27
+ ```bash
28
+ export UNDERSTAND_TOKEN="your-input-token"
29
+ export UNDERSTAND_ENV="production"
30
+ ```
31
+
32
+ Or configure it directly in code:
33
+
34
+ ```python
35
+ from understand import UnderstandConfig
36
+
37
+ config = UnderstandConfig(
38
+ token="your-input-token",
39
+ environment="production",
40
+ )
41
+ ```
42
+
43
+ ## Basic Usage
44
+
45
+ Send a log message:
46
+
47
+ ```python
48
+ from understand import UnderstandConfig, UnderstandLogger
49
+
50
+ logger = UnderstandLogger.from_config(
51
+ UnderstandConfig(token="your-input-token", environment="production")
52
+ )
53
+
54
+ logger.error("Understand.io test error", context={"source": "my-app"})
55
+ ```
56
+
57
+ Capture and send an exception:
58
+
59
+ ```python
60
+ from understand import UnderstandConfig, UnderstandLogger
61
+
62
+ logger = UnderstandLogger.from_config(
63
+ UnderstandConfig(token="your-input-token", environment="production")
64
+ )
65
+
66
+ try:
67
+ raise RuntimeError("Payment capture failed")
68
+ except RuntimeError as exc:
69
+ logger.exception(exc, context={"feature": "payments"})
70
+ ```
71
+
72
+ ## Stdlib Logging
73
+
74
+ Attach Understand to Python's built-in `logging` package:
75
+
76
+ ```python
77
+ import logging
78
+
79
+ from understand import UnderstandConfig, UnderstandLogger
80
+ from understand.integrations.logging import UnderstandLoggingHandler
81
+
82
+ understand_logger = UnderstandLogger.from_config(
83
+ UnderstandConfig.from_env()
84
+ )
85
+
86
+ logging.getLogger().addHandler(UnderstandLoggingHandler(understand_logger))
87
+ logging.getLogger().setLevel(logging.INFO)
88
+
89
+ logging.error("This error is sent to Understand.io")
90
+ ```
91
+
92
+ Exceptions logged with `exc_info=True` are sent as structured exception events:
93
+
94
+ ```python
95
+ try:
96
+ raise ValueError("Invalid checkout amount")
97
+ except ValueError:
98
+ logging.exception("Checkout failed")
99
+ ```
100
+
101
+ ## Request Context
102
+
103
+ For web applications, attach request-like metadata before logging:
104
+
105
+ ```python
106
+ from understand import RequestContext, UnderstandConfig, UnderstandLogger
107
+
108
+ logger = UnderstandLogger.from_config(
109
+ UnderstandConfig(token="your-input-token", environment="production")
110
+ )
111
+
112
+ logger.field_provider.set_request_context(
113
+ RequestContext(
114
+ path="/checkout/payments",
115
+ method="POST",
116
+ route_name="payments.create",
117
+ query={"debug": "1"},
118
+ body={"amount": 4999, "password": "hidden automatically"},
119
+ client_ip="203.0.113.10",
120
+ user_agent="Mozilla/5.0",
121
+ user_id="user-123",
122
+ session_id="session-id",
123
+ )
124
+ )
125
+ ```
126
+
127
+ Fields like `password`, `password_confirmation`, `access_token`, `secret_key`,
128
+ `token`, and `access_key` are replaced with `[value hidden]` by default.
129
+
130
+ Environment variables:
131
+
132
+ - `UNDERSTAND_TOKEN`
133
+ - `UNDERSTAND_ENABLED`
134
+ - `UNDERSTAND_API_URL`
135
+ - `UNDERSTAND_HANDLER`
136
+ - `UNDERSTAND_ENV`
137
+ - `UNDERSTAND_SQL`
138
+ - `UNDERSTAND_SQL_BINDINGS`
139
+ - `UNDERSTAND_QUERY_STRING`
140
+ - `UNDERSTAND_REQUEST_BODY`
141
+ - `UNDERSTAND_POST_DATA` as a SDK config-parity alias for request body collection
142
+ - `UNDERSTAND_HIDDEN_REQUEST_FIELDS`
143
+ - `UNDERSTAND_SSL_CA_BUNDLE`
144
+
145
+ For browser JavaScript error tracking, use the separate `understand-js` SDK.
146
+
147
+ ## Long-Running Workers
148
+
149
+ For long-running workers, call `reset_context()` between jobs to regenerate the
150
+ request token and clear collected data:
151
+
152
+ ```python
153
+ for job in jobs:
154
+ logger.reset_context()
155
+
156
+ try:
157
+ process_job(job)
158
+ except Exception as exc:
159
+ logger.exception(exc, context={"job_id": job.id})
160
+ ```
161
+
162
+ ## Local Testing Without Sending Data
163
+
164
+ Use `CallbackHandler` to inspect payloads without making network requests:
165
+
166
+ ```python
167
+ import json
168
+
169
+ from understand import CallbackHandler, UnderstandConfig, UnderstandLogger
170
+
171
+ events = []
172
+ logger = UnderstandLogger.from_config(
173
+ UnderstandConfig(token="test-token", environment="local"),
174
+ handler=CallbackHandler(lambda payload: events.append(json.loads(payload))),
175
+ )
176
+
177
+ logger.error("Local test error")
178
+
179
+ print(json.dumps(events, indent=2))
180
+ ```
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "understand-py"
7
+ version = "0.1.0"
8
+ description = "Framework-neutral Python SDK for sending logs and exceptions to Understand.io."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ {name = "Understand"}
15
+ ]
16
+ keywords = ["understand.io", "logging", "errors", "exceptions"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ ]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .collector import DataCollector
4
+ from .config import UnderstandConfig
5
+ from .encoder import ExceptionEncoder
6
+ from .fields import FieldProvider, RequestContext
7
+ from .handlers import AsyncHandler, BaseHandler, CallbackHandler, SyncHandler
8
+ from .logger import UnderstandLogger
9
+ from .tokens import TokenProvider
10
+
11
+ __all__ = [
12
+ "AsyncHandler",
13
+ "BaseHandler",
14
+ "CallbackHandler",
15
+ "DataCollector",
16
+ "ExceptionEncoder",
17
+ "FieldProvider",
18
+ "RequestContext",
19
+ "SyncHandler",
20
+ "TokenProvider",
21
+ "UnderstandConfig",
22
+ "UnderstandLogger",
23
+ "__version__",
24
+ ]
@@ -0,0 +1,27 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+
4
+ class DataCollector:
5
+ def __init__(self, limit: int = 50) -> None:
6
+ self._data: Dict[str, Any] = {}
7
+ self.limit = limit
8
+
9
+ def set_limit(self, limit: int) -> None:
10
+ self.limit = limit
11
+
12
+ def reset(self) -> None:
13
+ self._data = {}
14
+
15
+ def set(self, key: str, value: Any) -> None:
16
+ self._data[key] = value
17
+
18
+ def set_in_array(self, key: str, value: Any) -> None:
19
+ values: List[Any] = list(self._data.get(key, []))
20
+ if len(values) > self.limit - 1:
21
+ values.pop(0)
22
+ values.append(value)
23
+ self._data[key] = values
24
+
25
+ def get_by_key(self, key: str) -> Optional[Any]:
26
+ return self._data.get(key)
27
+
@@ -0,0 +1,82 @@
1
+ from dataclasses import dataclass, field
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Callable, Optional, Sequence, Tuple
5
+
6
+
7
+ DEFAULT_HIDDEN_FIELDS = (
8
+ "password",
9
+ "password_confirmation",
10
+ "access_token",
11
+ "secret_key",
12
+ "token",
13
+ "access_key",
14
+ )
15
+
16
+
17
+ def _parse_bool(value: Optional[str], default: bool) -> bool:
18
+ if value is None:
19
+ return default
20
+
21
+ normalized = value.strip().lower()
22
+ if normalized in {"1", "true", "yes", "y", "on"}:
23
+ return True
24
+ if normalized in {"0", "false", "no", "n", "off"}:
25
+ return False
26
+ return default
27
+
28
+
29
+ def _parse_csv(value: Optional[str], default: Sequence[str]) -> Tuple[str, ...]:
30
+ if value is None:
31
+ return tuple(default)
32
+
33
+ return tuple(part.strip() for part in value.split(",") if part.strip())
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class UnderstandConfig:
38
+ token: Optional[str] = None
39
+ enabled: bool = True
40
+ api_url: str = "https://api.understand.io"
41
+ handler: str = "sync"
42
+ project_root: Path = field(default_factory=Path.cwd)
43
+ environment: Optional[str] = None
44
+ sql_enabled: bool = True
45
+ sql_bindings: bool = False
46
+ query_string_enabled: bool = True
47
+ request_body_enabled: bool = True
48
+ hidden_fields: Tuple[str, ...] = DEFAULT_HIDDEN_FIELDS
49
+ ignored_logs: Tuple[str, ...] = ()
50
+ log_filter: Optional[Callable[[str, object, object], bool]] = None
51
+ timeout: float = 5.0
52
+ stack_trace_limit: int = 100
53
+ ssl_ca_bundle: Optional[str] = None
54
+
55
+ def __post_init__(self) -> None:
56
+ object.__setattr__(self, "project_root", Path(self.project_root))
57
+ object.__setattr__(self, "hidden_fields", tuple(self.hidden_fields))
58
+ object.__setattr__(self, "ignored_logs", tuple(self.ignored_logs))
59
+
60
+ @classmethod
61
+ def from_env(cls, project_root: Optional[Path] = None) -> "UnderstandConfig":
62
+ request_body_value = os.environ.get("UNDERSTAND_REQUEST_BODY")
63
+ if request_body_value is None:
64
+ request_body_value = os.environ.get("UNDERSTAND_POST_DATA")
65
+
66
+ return cls(
67
+ token=os.environ.get("UNDERSTAND_TOKEN"),
68
+ enabled=_parse_bool(os.environ.get("UNDERSTAND_ENABLED"), True),
69
+ api_url=os.environ.get("UNDERSTAND_API_URL", "https://api.understand.io"),
70
+ handler=os.environ.get("UNDERSTAND_HANDLER", "sync"),
71
+ project_root=project_root if project_root is not None else Path.cwd(),
72
+ environment=os.environ.get("UNDERSTAND_ENV"),
73
+ sql_enabled=_parse_bool(os.environ.get("UNDERSTAND_SQL"), True),
74
+ sql_bindings=_parse_bool(os.environ.get("UNDERSTAND_SQL_BINDINGS"), False),
75
+ query_string_enabled=_parse_bool(os.environ.get("UNDERSTAND_QUERY_STRING"), True),
76
+ request_body_enabled=_parse_bool(request_body_value, True),
77
+ hidden_fields=_parse_csv(
78
+ os.environ.get("UNDERSTAND_HIDDEN_REQUEST_FIELDS"),
79
+ DEFAULT_HIDDEN_FIELDS,
80
+ ),
81
+ ssl_ca_bundle=os.environ.get("UNDERSTAND_SSL_CA_BUNDLE"),
82
+ )
@@ -0,0 +1,96 @@
1
+ import inspect
2
+ from pathlib import Path
3
+ import traceback
4
+ from types import TracebackType
5
+ from typing import Any, Dict, Iterable, List, Optional
6
+
7
+ from .config import UnderstandConfig
8
+
9
+
10
+ class ExceptionEncoder:
11
+ def __init__(self, config: Optional[UnderstandConfig] = None) -> None:
12
+ self.config = config or UnderstandConfig()
13
+
14
+ def exception_to_dict(self, exception: BaseException) -> Dict[str, Any]:
15
+ if not isinstance(exception, BaseException):
16
+ raise TypeError("exception must be an instance of BaseException")
17
+
18
+ frames = self._traceback_to_frames(exception.__traceback__)
19
+ top = frames[-1] if frames else None
20
+ file_name = self._relative_path(top.filename) if top else None
21
+ line = top.lineno if top else None
22
+ class_name = exception.__class__.__name__
23
+ message = str(exception) if str(exception) else class_name
24
+
25
+ return {
26
+ "message": message,
27
+ "class": class_name,
28
+ "code": getattr(exception, "errno", None),
29
+ "file": file_name,
30
+ "line": line,
31
+ "stack": self.stack_trace_to_list(frames),
32
+ }
33
+
34
+ def set_current_stack_trace(self, error_log: Dict[str, Any]) -> Dict[str, Any]:
35
+ frames = [
36
+ frame
37
+ for frame in inspect.stack()[1 : self.config.stack_trace_limit + 1]
38
+ if "/understand/" not in frame.filename.replace("\\", "/")
39
+ ]
40
+ stack = self.stack_trace_to_list(frames)
41
+ if stack:
42
+ error_log["class"] = None
43
+ error_log["file"] = stack[0]["file"]
44
+ error_log["line"] = stack[0]["line"]
45
+ error_log["stack"] = stack
46
+ return error_log
47
+
48
+ def stack_trace_to_list(self, frames: Iterable[Any]) -> List[Dict[str, Any]]:
49
+ stack = []
50
+ for frame in list(frames)[-self.config.stack_trace_limit :]:
51
+ filename = getattr(frame, "filename", None)
52
+ lineno = getattr(frame, "lineno", None)
53
+ function = getattr(frame, "name", None) or getattr(frame, "function", None)
54
+ relative = self._relative_path(filename) if filename else None
55
+ stack.append(
56
+ {
57
+ "class": None,
58
+ "function": function,
59
+ "args": [],
60
+ "type": "function",
61
+ "file": relative,
62
+ "line": lineno,
63
+ "code": self.get_code(relative, lineno),
64
+ }
65
+ )
66
+ return stack
67
+
68
+ def get_code(self, relative_path: Optional[str], line: Optional[int], lines_around: int = 6) -> Optional[List[Dict[str, Any]]]:
69
+ if not relative_path or not line:
70
+ return None
71
+
72
+ path = self.config.project_root / relative_path
73
+ try:
74
+ lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
75
+ except OSError:
76
+ return None
77
+
78
+ start = max(1, line - lines_around)
79
+ end = min(len(lines), line + lines_around)
80
+ return [{"line": number, "code": lines[number - 1][:250].rstrip()} for number in range(start, end + 1)]
81
+
82
+ def _traceback_to_frames(self, tb: Optional[TracebackType]) -> List[traceback.FrameSummary]:
83
+ if tb is None:
84
+ return []
85
+ return traceback.extract_tb(tb)
86
+
87
+ def _relative_path(self, filename: Optional[str]) -> Optional[str]:
88
+ if filename is None:
89
+ return None
90
+
91
+ path = Path(filename)
92
+ try:
93
+ return str(path.resolve().relative_to(self.config.project_root.resolve()))
94
+ except ValueError:
95
+ return str(path)
96
+