bug-tracer-client 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.
- bug_tracer_client-0.1.0/.gitignore +17 -0
- bug_tracer_client-0.1.0/LICENSE +9 -0
- bug_tracer_client-0.1.0/PKG-INFO +87 -0
- bug_tracer_client-0.1.0/README.md +60 -0
- bug_tracer_client-0.1.0/bug_tracer_client/__init__.py +3 -0
- bug_tracer_client-0.1.0/bug_tracer_client/apps.py +7 -0
- bug_tracer_client-0.1.0/bug_tracer_client/client.py +22 -0
- bug_tracer_client-0.1.0/bug_tracer_client/conf.py +37 -0
- bug_tracer_client-0.1.0/bug_tracer_client/dsn.py +57 -0
- bug_tracer_client-0.1.0/bug_tracer_client/filters.py +34 -0
- bug_tracer_client-0.1.0/bug_tracer_client/middleware.py +23 -0
- bug_tracer_client-0.1.0/bug_tracer_client/payload.py +97 -0
- bug_tracer_client-0.1.0/bug_tracer_client/py.typed +0 -0
- bug_tracer_client-0.1.0/pyproject.toml +47 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.env
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.coverage
|
|
8
|
+
htmlcov/
|
|
9
|
+
.venv/
|
|
10
|
+
db.sqlite3
|
|
11
|
+
project/db.sqlite3
|
|
12
|
+
project/test_db.sqlite3
|
|
13
|
+
project/.pytest_cache/
|
|
14
|
+
project/.coverage
|
|
15
|
+
project/htmlcov/
|
|
16
|
+
project/.venv/
|
|
17
|
+
*.egg-info/
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bug-tracer-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django middleware client for sending unhandled exceptions to Bug Tracer
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/bug-tracer/bug-tracer-api
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/bug-tracer/bug-tracer-api
|
|
7
|
+
Project-URL: Issues, https://gitlab.com/bug-tracer/bug-tracer-api/-/issues
|
|
8
|
+
Author: Bug Tracer
|
|
9
|
+
License: Proprietary
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bug-tracker,django,errors,monitoring
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 5.1
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: Other/Proprietary License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: django<6.0,>=4.2
|
|
25
|
+
Requires-Dist: requests<3.0.0,>=2.32.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# bug-tracer-client
|
|
29
|
+
|
|
30
|
+
`bug-tracer-client` is a reusable Django app that captures unhandled exceptions and sends them to a Bug Tracer backend.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install bug-tracer-client
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Django setup
|
|
39
|
+
|
|
40
|
+
Add the app to `INSTALLED_APPS`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
INSTALLED_APPS = [
|
|
44
|
+
# ...
|
|
45
|
+
"bug_tracer_client",
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Add the middleware after authentication middleware so authenticated user context can be included:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
MIDDLEWARE = [
|
|
53
|
+
# ...
|
|
54
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
55
|
+
"bug_tracer_client.middleware.BugTracerMiddleware",
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Set the single required setting:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
BUG_TRACER_DSN = "bugtracer+https://PUBLIC_KEY@bug-tracer.example.com/1"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Supported DSN formats:
|
|
66
|
+
|
|
67
|
+
- `bugtracer+https://<public_key>@<host>[:<port>]/<project_id>`
|
|
68
|
+
- `bugtracer+http://<public_key>@<host>[:<port>]/<project_id>`
|
|
69
|
+
|
|
70
|
+
## What it sends
|
|
71
|
+
|
|
72
|
+
The middleware sends a best-effort JSON payload containing:
|
|
73
|
+
|
|
74
|
+
- exception type and message
|
|
75
|
+
- stacktrace frames
|
|
76
|
+
- request method and path
|
|
77
|
+
- optional authenticated user info
|
|
78
|
+
|
|
79
|
+
Sensitive fields such as `password`, `token`, `authorization`, and `cookie` are redacted before sending.
|
|
80
|
+
|
|
81
|
+
## Smoke check
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from bug_tracer_client import __version__
|
|
85
|
+
|
|
86
|
+
print(__version__)
|
|
87
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# bug-tracer-client
|
|
2
|
+
|
|
3
|
+
`bug-tracer-client` is a reusable Django app that captures unhandled exceptions and sends them to a Bug Tracer backend.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install bug-tracer-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Django setup
|
|
12
|
+
|
|
13
|
+
Add the app to `INSTALLED_APPS`:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
INSTALLED_APPS = [
|
|
17
|
+
# ...
|
|
18
|
+
"bug_tracer_client",
|
|
19
|
+
]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add the middleware after authentication middleware so authenticated user context can be included:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
MIDDLEWARE = [
|
|
26
|
+
# ...
|
|
27
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
28
|
+
"bug_tracer_client.middleware.BugTracerMiddleware",
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Set the single required setting:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
BUG_TRACER_DSN = "bugtracer+https://PUBLIC_KEY@bug-tracer.example.com/1"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Supported DSN formats:
|
|
39
|
+
|
|
40
|
+
- `bugtracer+https://<public_key>@<host>[:<port>]/<project_id>`
|
|
41
|
+
- `bugtracer+http://<public_key>@<host>[:<port>]/<project_id>`
|
|
42
|
+
|
|
43
|
+
## What it sends
|
|
44
|
+
|
|
45
|
+
The middleware sends a best-effort JSON payload containing:
|
|
46
|
+
|
|
47
|
+
- exception type and message
|
|
48
|
+
- stacktrace frames
|
|
49
|
+
- request method and path
|
|
50
|
+
- optional authenticated user info
|
|
51
|
+
|
|
52
|
+
Sensitive fields such as `password`, `token`, `authorization`, and `cookie` are redacted before sending.
|
|
53
|
+
|
|
54
|
+
## Smoke check
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from bug_tracer_client import __version__
|
|
58
|
+
|
|
59
|
+
print(__version__)
|
|
60
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from .conf import BugTracerSettings, get_bug_tracer_settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def send_event(payload: dict, config: BugTracerSettings | None = None, session=None) -> bool:
|
|
7
|
+
active_config = config or get_bug_tracer_settings()
|
|
8
|
+
if not active_config.parsed_dsn:
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
http_client = session or requests
|
|
12
|
+
try:
|
|
13
|
+
response = http_client.post(
|
|
14
|
+
active_config.parsed_dsn.ingest_url,
|
|
15
|
+
json=payload,
|
|
16
|
+
headers={"X-Bug-Tracker-DSN": active_config.parsed_dsn.header_dsn},
|
|
17
|
+
timeout=active_config.timeout,
|
|
18
|
+
)
|
|
19
|
+
except requests.RequestException:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
return bool(getattr(response, "ok", False))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
|
|
5
|
+
from .dsn import ParsedBugTracerDsn, parse_bug_tracer_dsn
|
|
6
|
+
|
|
7
|
+
DEFAULT_SENSITIVE_FIELDS = (
|
|
8
|
+
"password",
|
|
9
|
+
"passwd",
|
|
10
|
+
"secret",
|
|
11
|
+
"token",
|
|
12
|
+
"api_key",
|
|
13
|
+
"authorization",
|
|
14
|
+
"cookie",
|
|
15
|
+
)
|
|
16
|
+
DEFAULT_TIMEOUT = 1.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class BugTracerSettings:
|
|
21
|
+
parsed_dsn: ParsedBugTracerDsn | None
|
|
22
|
+
timeout: float
|
|
23
|
+
sensitive_fields: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_bug_tracer_settings() -> BugTracerSettings:
|
|
27
|
+
raw_dsn = getattr(settings, "BUG_TRACER_DSN", "")
|
|
28
|
+
try:
|
|
29
|
+
parsed_dsn = parse_bug_tracer_dsn(raw_dsn) if raw_dsn else None
|
|
30
|
+
except ValueError:
|
|
31
|
+
parsed_dsn = None
|
|
32
|
+
|
|
33
|
+
return BugTracerSettings(
|
|
34
|
+
parsed_dsn=parsed_dsn,
|
|
35
|
+
timeout=DEFAULT_TIMEOUT,
|
|
36
|
+
sensitive_fields=DEFAULT_SENSITIVE_FIELDS,
|
|
37
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from urllib.parse import urlsplit
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class ParsedBugTracerDsn:
|
|
7
|
+
ingest_url: str
|
|
8
|
+
header_dsn: str
|
|
9
|
+
public_key: str
|
|
10
|
+
host: str
|
|
11
|
+
project_id: str
|
|
12
|
+
transport: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_bug_tracer_dsn(raw_dsn: str) -> ParsedBugTracerDsn:
|
|
16
|
+
if not raw_dsn:
|
|
17
|
+
raise ValueError("BUG_TRACER_DSN is empty")
|
|
18
|
+
|
|
19
|
+
parsed = urlsplit(raw_dsn)
|
|
20
|
+
transport = _parse_transport(parsed.scheme)
|
|
21
|
+
public_key = parsed.username
|
|
22
|
+
host = parsed.hostname
|
|
23
|
+
project_id = _parse_project_id(parsed.path)
|
|
24
|
+
|
|
25
|
+
if not public_key:
|
|
26
|
+
raise ValueError("BUG_TRACER_DSN public key is required")
|
|
27
|
+
if parsed.password:
|
|
28
|
+
raise ValueError("BUG_TRACER_DSN password is not supported")
|
|
29
|
+
if not host:
|
|
30
|
+
raise ValueError("BUG_TRACER_DSN host is required")
|
|
31
|
+
if parsed.query or parsed.fragment:
|
|
32
|
+
raise ValueError("BUG_TRACER_DSN query and fragment are not supported")
|
|
33
|
+
|
|
34
|
+
netloc = parsed.netloc.rsplit("@", 1)[-1]
|
|
35
|
+
ingest_url = f"{transport}://{netloc}/api/v1/ingest/"
|
|
36
|
+
header_dsn = f"bugtracer://{public_key}@{netloc}/{project_id}"
|
|
37
|
+
return ParsedBugTracerDsn(
|
|
38
|
+
ingest_url=ingest_url,
|
|
39
|
+
header_dsn=header_dsn,
|
|
40
|
+
public_key=public_key,
|
|
41
|
+
host=host,
|
|
42
|
+
project_id=project_id,
|
|
43
|
+
transport=transport,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_transport(scheme: str) -> str:
|
|
48
|
+
if scheme not in {"bugtracer+http", "bugtracer+https"}:
|
|
49
|
+
raise ValueError("BUG_TRACER_DSN scheme must be bugtracer+http or bugtracer+https")
|
|
50
|
+
return scheme.removeprefix("bugtracer+")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_project_id(path: str) -> str:
|
|
54
|
+
project_id = path.lstrip("/")
|
|
55
|
+
if not project_id or "/" in project_id:
|
|
56
|
+
raise ValueError("BUG_TRACER_DSN path must contain a single project id")
|
|
57
|
+
return project_id
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
REDACTED = "[REDACTED]"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sanitize_data(value, sensitive_fields: tuple[str, ...]):
|
|
7
|
+
normalized_sensitive_fields = {normalize_key(field) for field in sensitive_fields}
|
|
8
|
+
return _sanitize_value(value, normalized_sensitive_fields)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _sanitize_value(value, sensitive_fields: set[str]):
|
|
12
|
+
if isinstance(value, Mapping):
|
|
13
|
+
return {
|
|
14
|
+
key: (
|
|
15
|
+
REDACTED
|
|
16
|
+
if is_sensitive_key(key, sensitive_fields)
|
|
17
|
+
else _sanitize_value(item, sensitive_fields)
|
|
18
|
+
)
|
|
19
|
+
for key, item in value.items()
|
|
20
|
+
}
|
|
21
|
+
if isinstance(value, list):
|
|
22
|
+
return [_sanitize_value(item, sensitive_fields) for item in value]
|
|
23
|
+
if isinstance(value, tuple):
|
|
24
|
+
return tuple(_sanitize_value(item, sensitive_fields) for item in value)
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_sensitive_key(key, sensitive_fields: set[str]) -> bool:
|
|
29
|
+
normalized_key = normalize_key(key)
|
|
30
|
+
return any(field and field in normalized_key for field in sensitive_fields)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_key(key) -> str:
|
|
34
|
+
return "".join(ch.lower() for ch in str(key) if ch.isalnum())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .client import send_event
|
|
2
|
+
from .conf import get_bug_tracer_settings
|
|
3
|
+
from .payload import build_event_payload
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BugTracerMiddleware:
|
|
7
|
+
def __init__(self, get_response):
|
|
8
|
+
self.get_response = get_response
|
|
9
|
+
|
|
10
|
+
def __call__(self, request):
|
|
11
|
+
try:
|
|
12
|
+
return self.get_response(request)
|
|
13
|
+
except Exception as exc:
|
|
14
|
+
self.capture_exception(request, exc)
|
|
15
|
+
raise
|
|
16
|
+
|
|
17
|
+
def capture_exception(self, request, exc: Exception) -> None:
|
|
18
|
+
config = get_bug_tracer_settings()
|
|
19
|
+
try:
|
|
20
|
+
payload = build_event_payload(request, exc, config)
|
|
21
|
+
send_event(payload, config=config)
|
|
22
|
+
except Exception:
|
|
23
|
+
return
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import traceback
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from django.http import QueryDict
|
|
6
|
+
|
|
7
|
+
from .conf import BugTracerSettings
|
|
8
|
+
from .filters import sanitize_data
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_event_payload(request, exc: Exception, config: BugTracerSettings) -> dict:
|
|
12
|
+
payload = {
|
|
13
|
+
"message": str(exc) or exc.__class__.__name__,
|
|
14
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
15
|
+
"exception": {
|
|
16
|
+
"type": exc.__class__.__name__,
|
|
17
|
+
"value": str(exc),
|
|
18
|
+
"stacktrace": {"frames": extract_frames(exc)},
|
|
19
|
+
},
|
|
20
|
+
"request": build_request_payload(request),
|
|
21
|
+
}
|
|
22
|
+
user_payload = build_user_payload(request)
|
|
23
|
+
if user_payload:
|
|
24
|
+
payload["user"] = user_payload
|
|
25
|
+
return sanitize_data(payload, config.sensitive_fields)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_frames(exc: Exception) -> list[dict[str, object | None]]:
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
"filename": frame.filename,
|
|
32
|
+
"function": frame.name,
|
|
33
|
+
"lineno": frame.lineno,
|
|
34
|
+
"context_line": frame.line,
|
|
35
|
+
}
|
|
36
|
+
for frame in traceback.extract_tb(exc.__traceback__)
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_request_payload(request) -> dict:
|
|
41
|
+
payload = {
|
|
42
|
+
"method": request.method,
|
|
43
|
+
"path": request.path,
|
|
44
|
+
"headers": build_headers(request),
|
|
45
|
+
}
|
|
46
|
+
query_params = querydict_to_dict(request.GET)
|
|
47
|
+
if query_params:
|
|
48
|
+
payload["query_params"] = query_params
|
|
49
|
+
body_data = build_request_data(request)
|
|
50
|
+
if body_data is not None:
|
|
51
|
+
payload["data"] = body_data
|
|
52
|
+
return payload
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_headers(request) -> dict:
|
|
56
|
+
headers = {}
|
|
57
|
+
for key, value in request.META.items():
|
|
58
|
+
if key.startswith("HTTP_"):
|
|
59
|
+
header_name = key[5:].replace("_", "-").title()
|
|
60
|
+
headers[header_name] = value
|
|
61
|
+
elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"}:
|
|
62
|
+
headers[key.replace("_", "-").title()] = value
|
|
63
|
+
return headers
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_request_data(request):
|
|
67
|
+
if request.content_type == "application/json":
|
|
68
|
+
raw_body = request.body.decode("utf-8", errors="replace").strip()
|
|
69
|
+
if not raw_body:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(raw_body)
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
return {"raw_body": raw_body}
|
|
75
|
+
if request.POST:
|
|
76
|
+
return querydict_to_dict(request.POST)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_user_payload(request) -> dict | None:
|
|
81
|
+
user = getattr(request, "user", None)
|
|
82
|
+
if not user or not getattr(user, "is_authenticated", False):
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
payload = {"id": user.pk, "username": user.get_username()}
|
|
86
|
+
email = getattr(user, "email", "")
|
|
87
|
+
if email:
|
|
88
|
+
payload["email"] = email
|
|
89
|
+
return payload
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def querydict_to_dict(querydict: QueryDict) -> dict:
|
|
93
|
+
data = {}
|
|
94
|
+
for key in querydict.keys():
|
|
95
|
+
values = querydict.getlist(key)
|
|
96
|
+
data[key] = values if len(values) > 1 else values[0]
|
|
97
|
+
return data
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bug-tracer-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Django middleware client for sending unhandled exceptions to Bug Tracer"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Bug Tracer" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["django", "errors", "bug-tracker", "monitoring"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Framework :: Django",
|
|
19
|
+
"Framework :: Django :: 5.1",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: Other/Proprietary License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"Django>=4.2,<6.0",
|
|
31
|
+
"requests>=2.32.0,<3.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://gitlab.com/bug-tracer/bug-tracer-api"
|
|
36
|
+
Repository = "https://gitlab.com/bug-tracer/bug-tracer-api"
|
|
37
|
+
Issues = "https://gitlab.com/bug-tracer/bug-tracer-api/-/issues"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["bug_tracer_client"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = [
|
|
44
|
+
"bug_tracer_client",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
]
|