errorgap 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,18 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ python-version: ["3.9", "3.12"]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - run: pip install -e ".[dev]"
18
+ - run: pytest
@@ -0,0 +1,17 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ jobs:
6
+ publish:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install build && python -m build
17
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ build/
9
+ dist/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
13
+ .env
14
+ .env.*
15
+ !.env.example
errorgap-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Errorgap
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,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: errorgap
3
+ Version: 0.1.0
4
+ Summary: Python notifier for Errorgap error tracking.
5
+ Project-URL: Homepage, https://github.com/errorgaphq/errorgap-python
6
+ Project-URL: Source, https://github.com/errorgaphq/errorgap-python
7
+ Author: Errorgap
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: error-tracking,errorgap,exceptions,monitoring
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Bug Tracking
20
+ Requires-Python: >=3.9
21
+ Provides-Extra: dev
22
+ Requires-Dist: django>=4.2; extra == 'dev'
23
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
24
+ Requires-Dist: flask>=2.3; extra == 'dev'
25
+ Requires-Dist: httpx>=0.27; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: starlette>=0.27; extra == 'dev'
28
+ Provides-Extra: django
29
+ Requires-Dist: django>=4.2; extra == 'django'
30
+ Provides-Extra: fastapi
31
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
32
+ Requires-Dist: starlette>=0.27; extra == 'fastapi'
33
+ Provides-Extra: flask
34
+ Requires-Dist: flask>=2.3; extra == 'flask'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # errorgap
38
+
39
+ Python notifier for [Errorgap](https://errorgap.com). Captures exceptions,
40
+ normalizes tracebacks, and ships notices to an Errorgap server. Single
41
+ package covers plain Python, Django, Flask, and FastAPI.
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ pip install errorgap
47
+ ```
48
+
49
+ For framework integrations:
50
+
51
+ ```sh
52
+ pip install "errorgap[django]" # or [flask], [fastapi]
53
+ ```
54
+
55
+ Requires Python 3.9+.
56
+
57
+ ## Configure
58
+
59
+ ```python
60
+ import errorgap
61
+
62
+ errorgap.init(
63
+ endpoint=...,
64
+ project_slug=...,
65
+ api_key=...,
66
+ environment="production",
67
+ )
68
+ ```
69
+
70
+ `init` reads the same values from `ERRORGAP_ENDPOINT`,
71
+ `ERRORGAP_PROJECT_SLUG`, `ERRORGAP_PROJECT_ID`, and `ERRORGAP_API_KEY` if you
72
+ don't pass them. By default it installs a `sys.excepthook` to catch
73
+ uncaught exceptions in scripts; pass `capture_globals=False` to skip.
74
+
75
+ ## Manual notification
76
+
77
+ ```python
78
+ try:
79
+ risky()
80
+ except Exception as exc:
81
+ errorgap.notify(exc, context={"component": "billing"})
82
+ raise
83
+ ```
84
+
85
+ `notify` returns a `DeliveryResult` (`status`, `body`, `error`, `queued`).
86
+ The SDK never raises.
87
+
88
+ ## Django
89
+
90
+ Add to `MIDDLEWARE`:
91
+
92
+ ```python
93
+ MIDDLEWARE = [
94
+ "errorgap.django.ErrorgapMiddleware",
95
+ # ... your existing middleware ...
96
+ ]
97
+ ```
98
+
99
+ ## Flask
100
+
101
+ ```python
102
+ from flask import Flask
103
+ from errorgap.flask import init_app
104
+
105
+ app = Flask(__name__)
106
+ init_app(app)
107
+ ```
108
+
109
+ ## FastAPI
110
+
111
+ ```python
112
+ from fastapi import FastAPI
113
+ from errorgap.fastapi import ErrorgapMiddleware
114
+
115
+ app = FastAPI()
116
+ app.add_middleware(ErrorgapMiddleware)
117
+ ```
118
+
119
+ ## Configuration reference
120
+
121
+ | Argument | Default | Notes |
122
+ |---|---|---|
123
+ | `endpoint` | `ERRORGAP_ENDPOINT` or `http://127.0.0.1:3030` | Base URL, no trailing slash |
124
+ | `project_slug` | `ERRORGAP_PROJECT_SLUG` | **Required** |
125
+ | `project_id` | `ERRORGAP_PROJECT_ID` | Optional, embedded in payload |
126
+ | `api_key` | `ERRORGAP_API_KEY` | Sent as `x-errorgap-project-key` |
127
+ | `environment` | `ERRORGAP_ENVIRONMENT`, `ENV`, or `development` | |
128
+ | `root_directory` | `os.getcwd()` | Used to mark frames as `in_app` |
129
+ | `async_` | `True` | Background-thread delivery |
130
+ | `logger` | `logging.getLogger("errorgap")` | Pass `None` to silence |
131
+ | `filter_keys` | `("password", "token", ...)` | Substring match, case-insensitive |
132
+ | `capture_globals` | `True` | Install `sys.excepthook` |
133
+
134
+ ## Graceful shutdown
135
+
136
+ ```python
137
+ errorgap.flush(timeout=5) # wait for queued deliveries
138
+ errorgap.shutdown(timeout=5) # flush + tear down background thread
139
+ ```
140
+
141
+ ## Verify
142
+
143
+ ```sh
144
+ curl -sS -X POST "$ERRORGAP_ENDPOINT/api/projects/$ERRORGAP_PROJECT_SLUG/notices" \
145
+ -H "content-type: application/json" \
146
+ -H "x-errorgap-project-key: $ERRORGAP_API_KEY" \
147
+ -d '{"errors":[{"type":"ErrorgapInstallTest","message":"Errorgap install verification"}],"context":{"environment":"development"}}'
148
+ ```
149
+
150
+ Then trigger a real error and confirm it appears in the Errorgap UI.
151
+
152
+ ## Development
153
+
154
+ ```sh
155
+ python -m venv .venv
156
+ source .venv/bin/activate
157
+ pip install -e ".[dev]"
158
+ pytest
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT.
@@ -0,0 +1,127 @@
1
+ # errorgap
2
+
3
+ Python notifier for [Errorgap](https://errorgap.com). Captures exceptions,
4
+ normalizes tracebacks, and ships notices to an Errorgap server. Single
5
+ package covers plain Python, Django, Flask, and FastAPI.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install errorgap
11
+ ```
12
+
13
+ For framework integrations:
14
+
15
+ ```sh
16
+ pip install "errorgap[django]" # or [flask], [fastapi]
17
+ ```
18
+
19
+ Requires Python 3.9+.
20
+
21
+ ## Configure
22
+
23
+ ```python
24
+ import errorgap
25
+
26
+ errorgap.init(
27
+ endpoint=...,
28
+ project_slug=...,
29
+ api_key=...,
30
+ environment="production",
31
+ )
32
+ ```
33
+
34
+ `init` reads the same values from `ERRORGAP_ENDPOINT`,
35
+ `ERRORGAP_PROJECT_SLUG`, `ERRORGAP_PROJECT_ID`, and `ERRORGAP_API_KEY` if you
36
+ don't pass them. By default it installs a `sys.excepthook` to catch
37
+ uncaught exceptions in scripts; pass `capture_globals=False` to skip.
38
+
39
+ ## Manual notification
40
+
41
+ ```python
42
+ try:
43
+ risky()
44
+ except Exception as exc:
45
+ errorgap.notify(exc, context={"component": "billing"})
46
+ raise
47
+ ```
48
+
49
+ `notify` returns a `DeliveryResult` (`status`, `body`, `error`, `queued`).
50
+ The SDK never raises.
51
+
52
+ ## Django
53
+
54
+ Add to `MIDDLEWARE`:
55
+
56
+ ```python
57
+ MIDDLEWARE = [
58
+ "errorgap.django.ErrorgapMiddleware",
59
+ # ... your existing middleware ...
60
+ ]
61
+ ```
62
+
63
+ ## Flask
64
+
65
+ ```python
66
+ from flask import Flask
67
+ from errorgap.flask import init_app
68
+
69
+ app = Flask(__name__)
70
+ init_app(app)
71
+ ```
72
+
73
+ ## FastAPI
74
+
75
+ ```python
76
+ from fastapi import FastAPI
77
+ from errorgap.fastapi import ErrorgapMiddleware
78
+
79
+ app = FastAPI()
80
+ app.add_middleware(ErrorgapMiddleware)
81
+ ```
82
+
83
+ ## Configuration reference
84
+
85
+ | Argument | Default | Notes |
86
+ |---|---|---|
87
+ | `endpoint` | `ERRORGAP_ENDPOINT` or `http://127.0.0.1:3030` | Base URL, no trailing slash |
88
+ | `project_slug` | `ERRORGAP_PROJECT_SLUG` | **Required** |
89
+ | `project_id` | `ERRORGAP_PROJECT_ID` | Optional, embedded in payload |
90
+ | `api_key` | `ERRORGAP_API_KEY` | Sent as `x-errorgap-project-key` |
91
+ | `environment` | `ERRORGAP_ENVIRONMENT`, `ENV`, or `development` | |
92
+ | `root_directory` | `os.getcwd()` | Used to mark frames as `in_app` |
93
+ | `async_` | `True` | Background-thread delivery |
94
+ | `logger` | `logging.getLogger("errorgap")` | Pass `None` to silence |
95
+ | `filter_keys` | `("password", "token", ...)` | Substring match, case-insensitive |
96
+ | `capture_globals` | `True` | Install `sys.excepthook` |
97
+
98
+ ## Graceful shutdown
99
+
100
+ ```python
101
+ errorgap.flush(timeout=5) # wait for queued deliveries
102
+ errorgap.shutdown(timeout=5) # flush + tear down background thread
103
+ ```
104
+
105
+ ## Verify
106
+
107
+ ```sh
108
+ curl -sS -X POST "$ERRORGAP_ENDPOINT/api/projects/$ERRORGAP_PROJECT_SLUG/notices" \
109
+ -H "content-type: application/json" \
110
+ -H "x-errorgap-project-key: $ERRORGAP_API_KEY" \
111
+ -d '{"errors":[{"type":"ErrorgapInstallTest","message":"Errorgap install verification"}],"context":{"environment":"development"}}'
112
+ ```
113
+
114
+ Then trigger a real error and confirm it appears in the Errorgap UI.
115
+
116
+ ## Development
117
+
118
+ ```sh
119
+ python -m venv .venv
120
+ source .venv/bin/activate
121
+ pip install -e ".[dev]"
122
+ pytest
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT.
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "errorgap"
7
+ version = "0.1.0"
8
+ description = "Python notifier for Errorgap error tracking."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Errorgap" }]
12
+ requires-python = ">=3.9"
13
+ keywords = ["errorgap", "error-tracking", "exceptions", "monitoring"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Bug Tracking",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.optional-dependencies]
28
+ django = ["django>=4.2"]
29
+ flask = ["flask>=2.3"]
30
+ fastapi = ["fastapi>=0.100", "starlette>=0.27"]
31
+ dev = [
32
+ "pytest>=8",
33
+ "django>=4.2",
34
+ "flask>=2.3",
35
+ "fastapi>=0.100",
36
+ "httpx>=0.27",
37
+ "starlette>=0.27",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/errorgaphq/errorgap-python"
42
+ Source = "https://github.com/errorgaphq/errorgap-python"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/errorgap"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ addopts = "-ra"
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .client import Client, DeliveryResult
6
+ from .configuration import Configuration
7
+ from .handlers import install_excepthook, uninstall_excepthook
8
+ from .version import VERSION
9
+
10
+ __all__ = [
11
+ "init",
12
+ "notify",
13
+ "flush",
14
+ "shutdown",
15
+ "configuration",
16
+ "client",
17
+ "Configuration",
18
+ "Client",
19
+ "DeliveryResult",
20
+ "VERSION",
21
+ ]
22
+
23
+ _configuration = Configuration()
24
+ _client = Client(_configuration)
25
+
26
+
27
+ def init(
28
+ *,
29
+ endpoint: Optional[str] = None,
30
+ project_slug: Optional[str] = None,
31
+ project_id: Optional[str] = None,
32
+ api_key: Optional[str] = None,
33
+ environment: Optional[str] = None,
34
+ root_directory: Optional[str] = None,
35
+ async_: Optional[bool] = None,
36
+ filter_keys: Optional[tuple] = None,
37
+ logger: Optional[Any] = None,
38
+ capture_globals: bool = True,
39
+ ) -> None:
40
+ """Configure the SDK and install global error hooks.
41
+
42
+ Reads ``ERRORGAP_ENDPOINT``, ``ERRORGAP_PROJECT_SLUG``,
43
+ ``ERRORGAP_PROJECT_ID``, and ``ERRORGAP_API_KEY`` from the environment when
44
+ the matching keyword argument is omitted.
45
+ """
46
+ global _configuration, _client
47
+
48
+ overrides: Dict[str, Any] = {}
49
+ if endpoint is not None:
50
+ overrides["endpoint"] = endpoint
51
+ if project_slug is not None:
52
+ overrides["project_slug"] = project_slug
53
+ if project_id is not None:
54
+ overrides["project_id"] = project_id
55
+ if api_key is not None:
56
+ overrides["api_key"] = api_key
57
+ if environment is not None:
58
+ overrides["environment"] = environment
59
+ if root_directory is not None:
60
+ overrides["root_directory"] = root_directory
61
+ if async_ is not None:
62
+ overrides["async_"] = async_
63
+ if filter_keys is not None:
64
+ overrides["filter_keys"] = filter_keys
65
+ if logger is not None:
66
+ overrides["logger"] = logger
67
+
68
+ _configuration = Configuration(**overrides)
69
+ _client.configure(_configuration)
70
+
71
+ if capture_globals:
72
+ install_excepthook(_client)
73
+ else:
74
+ uninstall_excepthook()
75
+
76
+
77
+ def notify(
78
+ exc: BaseException,
79
+ context: Optional[Dict[str, Any]] = None,
80
+ environment: Optional[Dict[str, Any]] = None,
81
+ session: Optional[Dict[str, Any]] = None,
82
+ params: Optional[Dict[str, Any]] = None,
83
+ sync: bool = False,
84
+ ) -> DeliveryResult:
85
+ return _client.notify(
86
+ exc,
87
+ context=context,
88
+ environment=environment,
89
+ session=session,
90
+ params=params,
91
+ sync=sync,
92
+ )
93
+
94
+
95
+ def flush(timeout: Optional[float] = None) -> None:
96
+ _client.flush(timeout)
97
+
98
+
99
+ def shutdown(timeout: Optional[float] = None) -> None:
100
+ _client.shutdown(timeout)
101
+
102
+
103
+ def configuration() -> Configuration:
104
+ return _configuration
105
+
106
+
107
+ def client() -> Client:
108
+ return _client
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import traceback
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ def parse_backtrace(exc: BaseException, root_directory: str) -> List[Dict[str, Any]]:
9
+ frames: List[Dict[str, Any]] = []
10
+ tb_frames = traceback.extract_tb(exc.__traceback__)
11
+
12
+ for index, frame in enumerate(tb_frames):
13
+ file = frame.filename
14
+ frames.append(
15
+ {
16
+ "file": _relative_file(file, root_directory),
17
+ "line": frame.lineno,
18
+ "function": frame.name,
19
+ "in_app": _is_in_app(file, root_directory),
20
+ "index": index,
21
+ }
22
+ )
23
+
24
+ return frames
25
+
26
+
27
+ def _relative_file(file: Optional[str], root: str) -> Optional[str]:
28
+ if not file:
29
+ return file
30
+ if not root:
31
+ return file
32
+ normalized = root if root.endswith(os.sep) else root + os.sep
33
+ if file.startswith(normalized):
34
+ return file[len(normalized):]
35
+ return file
36
+
37
+
38
+ def _is_in_app(file: Optional[str], root: str) -> bool:
39
+ if not file or not root:
40
+ return False
41
+ if "site-packages" in file:
42
+ return False
43
+ if file.startswith("<"):
44
+ return False
45
+ return file.startswith(root)