bidar 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.
bidar-0.1.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CyberHuginn
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.
bidar-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: bidar
3
+ Version: 0.1.0
4
+ Summary: Background error reporting SDK for Python and Django
5
+ Author-email: CyberHuginn <contact@cyberhuginn.com>
6
+ Maintainer-email: CyberHuginn <contact@cyberhuginn.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://cyberhuginn.com
9
+ Project-URL: Organization, https://fardadnoavar.ir
10
+ Keywords: bidar,error-reporting,django,monitoring,sentry
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: requests>=2.28
15
+ Provides-Extra: django
16
+ Requires-Dist: Django>=3.2; extra == "django"
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7; extra == "dev"
19
+ Requires-Dist: pytest-mock>=3; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # bidar-sdk-python
23
+
24
+ A lightweight error and event reporting SDK for Python applications, with optional Django integration. Reports are submitted in the background so application request handling does not wait for the remote API.
25
+
26
+ **Author:** CyberHuginn
27
+ **Organization:** Fardad Noavar
28
+ **Website:** [cyberhuginn.com](https://cyberhuginn.com)
29
+ **Organization website:** [fardadnoavar.ir](https://fardadnoavar.ir)
30
+ **Contact:** contact@cyberhuginn.com
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install bidar-sdk-python
36
+ ```
37
+
38
+ For Django projects:
39
+
40
+ ```bash
41
+ pip install "bidar-sdk-python[django]"
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ import bidar
48
+
49
+ bidar.init(
50
+ url="https://api.bidarhq.ir/api/v1/errors/",
51
+ access_key="YOUR_ACCESS_KEY",
52
+ project_name="billing-api",
53
+ environment="Production",
54
+ log_level="error",
55
+ timeout=3.0,
56
+ )
57
+
58
+ try:
59
+ run_job()
60
+ except Exception as exc:
61
+ bidar.capture_exception(exc)
62
+ ```
63
+
64
+ Reports are queued on a background worker. `capture_exception`, `capture_message`, and `capture_event` return immediately with a boolean indicating whether the event was accepted for processing.
65
+
66
+ ## Django integration
67
+
68
+ Add the middleware to `settings.py`:
69
+
70
+ ```python
71
+ MIDDLEWARE = [
72
+ # ...
73
+ "bidar.middleware.BidarMiddleware",
74
+ ]
75
+ ```
76
+
77
+ Configure the SDK:
78
+
79
+ ```python
80
+ BIDAR_SDK = {
81
+ "URL": "https://api.bidarhq.ir/api/v1/errors/",
82
+ "ACCESS_KEY": "YOUR_ACCESS_KEY",
83
+ "PROJECT_NAME": "my-django-app",
84
+ "ENVIRONMENT": "Production",
85
+ "LOG_LEVEL": "error",
86
+ "TIMEOUT": 3.0,
87
+ "ENABLED": True,
88
+ "MIDDLEWARE_ENABLED": True,
89
+ "CAPTURE_REQUEST_BODY": False,
90
+ "DEDUPLICATION_WINDOW": 30,
91
+ "MAX_WORKERS": 2,
92
+ }
93
+ ```
94
+
95
+ The middleware reports unhandled exceptions and re-raises the original exception. Set `MIDDLEWARE_ENABLED` to `False` to disable automatic middleware reporting without removing the middleware from `MIDDLEWARE`.
96
+
97
+ The SDK is initialized lazily from `BIDAR_SDK` when the middleware first runs. You can also call `bidar_sdk.init(...)` explicitly during application startup.
98
+
99
+ ## Manual reporting
100
+
101
+ ```python
102
+ import bidar
103
+
104
+ bidar.capture_exception(error)
105
+
106
+ bidar.capture_message(
107
+ "Payment provider is unavailable",
108
+ level="warning",
109
+ )
110
+
111
+ bidar.capture_event(
112
+ {"event": "subscription.renewal_failed", "customer_id": 42},
113
+ level="error",
114
+ )
115
+ ```
116
+
117
+ ## Configuration
118
+
119
+ | Option | Default | Description |
120
+ |---|---:|---|
121
+ | `url` | Required | HTTP endpoint that receives JSON POST reports |
122
+ | `access_key` | Required | API key sent in `X-Bidar-Access-Key` |
123
+ | `log_level` | `error` | One of `info`, `warning`, or `error` |
124
+ | `project_name` | `None` | Project identifier attached to every event |
125
+ | `environment` | `Production` | Deployment environment |
126
+ | `timeout` | `3.0` | Outbound HTTP timeout in seconds |
127
+ | `enabled` | `True` | Enables or disables all reporting |
128
+ | `middleware_enabled` | `True` | Enables or disables Django middleware reporting |
129
+ | `capture_request_body` | `False` | Opt-in request body capture |
130
+ | `deduplication_window` | `30` | Suppresses identical events within this many seconds; set to `0` to disable |
131
+ | `max_workers` | `2` | Number of background HTTP workers |
132
+
133
+ Log levels are threshold-based: `info` accepts all levels, `warning` accepts warnings and errors, and `error` accepts errors only.
134
+
135
+ ## Reliability and privacy
136
+
137
+ - Network requests run in a background thread pool.
138
+ - HTTP failures, serialization errors, and worker failures are contained and do not propagate into application code.
139
+ - `flush()` waits for submitted reports; `shutdown()` flushes and closes the client.
140
+ - Duplicate reports with the same event fingerprint are suppressed during the configured deduplication window.
141
+ - Authorization, cookies, passwords, tokens, secrets, and API-key-like fields are redacted.
142
+ - Request bodies are not captured by default. Enable `CAPTURE_REQUEST_BODY` only after reviewing the data your application sends.
143
+ - The SDK does not intentionally collect local variables or environment variables.
144
+
145
+ ## Lifecycle
146
+
147
+ ```python
148
+ bidar_sdk.flush()
149
+ bidar_sdk.shutdown()
150
+ ```
151
+
152
+ Call `flush()` during controlled shutdown if pending reports should be given a chance to finish. `shutdown()` prevents further submissions until the SDK is initialized again.
153
+
154
+ ## License
155
+
156
+ MIT
bidar-0.1.0/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # bidar-sdk-python
2
+
3
+ A lightweight error and event reporting SDK for Python applications, with optional Django integration. Reports are submitted in the background so application request handling does not wait for the remote API.
4
+
5
+ **Author:** CyberHuginn
6
+ **Organization:** Fardad Noavar
7
+ **Website:** [cyberhuginn.com](https://cyberhuginn.com)
8
+ **Organization website:** [fardadnoavar.ir](https://fardadnoavar.ir)
9
+ **Contact:** contact@cyberhuginn.com
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install bidar-sdk-python
15
+ ```
16
+
17
+ For Django projects:
18
+
19
+ ```bash
20
+ pip install "bidar-sdk-python[django]"
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ import bidar
27
+
28
+ bidar.init(
29
+ url="https://api.bidarhq.ir/api/v1/errors/",
30
+ access_key="YOUR_ACCESS_KEY",
31
+ project_name="billing-api",
32
+ environment="Production",
33
+ log_level="error",
34
+ timeout=3.0,
35
+ )
36
+
37
+ try:
38
+ run_job()
39
+ except Exception as exc:
40
+ bidar.capture_exception(exc)
41
+ ```
42
+
43
+ Reports are queued on a background worker. `capture_exception`, `capture_message`, and `capture_event` return immediately with a boolean indicating whether the event was accepted for processing.
44
+
45
+ ## Django integration
46
+
47
+ Add the middleware to `settings.py`:
48
+
49
+ ```python
50
+ MIDDLEWARE = [
51
+ # ...
52
+ "bidar.middleware.BidarMiddleware",
53
+ ]
54
+ ```
55
+
56
+ Configure the SDK:
57
+
58
+ ```python
59
+ BIDAR_SDK = {
60
+ "URL": "https://api.bidarhq.ir/api/v1/errors/",
61
+ "ACCESS_KEY": "YOUR_ACCESS_KEY",
62
+ "PROJECT_NAME": "my-django-app",
63
+ "ENVIRONMENT": "Production",
64
+ "LOG_LEVEL": "error",
65
+ "TIMEOUT": 3.0,
66
+ "ENABLED": True,
67
+ "MIDDLEWARE_ENABLED": True,
68
+ "CAPTURE_REQUEST_BODY": False,
69
+ "DEDUPLICATION_WINDOW": 30,
70
+ "MAX_WORKERS": 2,
71
+ }
72
+ ```
73
+
74
+ The middleware reports unhandled exceptions and re-raises the original exception. Set `MIDDLEWARE_ENABLED` to `False` to disable automatic middleware reporting without removing the middleware from `MIDDLEWARE`.
75
+
76
+ The SDK is initialized lazily from `BIDAR_SDK` when the middleware first runs. You can also call `bidar_sdk.init(...)` explicitly during application startup.
77
+
78
+ ## Manual reporting
79
+
80
+ ```python
81
+ import bidar
82
+
83
+ bidar.capture_exception(error)
84
+
85
+ bidar.capture_message(
86
+ "Payment provider is unavailable",
87
+ level="warning",
88
+ )
89
+
90
+ bidar.capture_event(
91
+ {"event": "subscription.renewal_failed", "customer_id": 42},
92
+ level="error",
93
+ )
94
+ ```
95
+
96
+ ## Configuration
97
+
98
+ | Option | Default | Description |
99
+ |---|---:|---|
100
+ | `url` | Required | HTTP endpoint that receives JSON POST reports |
101
+ | `access_key` | Required | API key sent in `X-Bidar-Access-Key` |
102
+ | `log_level` | `error` | One of `info`, `warning`, or `error` |
103
+ | `project_name` | `None` | Project identifier attached to every event |
104
+ | `environment` | `Production` | Deployment environment |
105
+ | `timeout` | `3.0` | Outbound HTTP timeout in seconds |
106
+ | `enabled` | `True` | Enables or disables all reporting |
107
+ | `middleware_enabled` | `True` | Enables or disables Django middleware reporting |
108
+ | `capture_request_body` | `False` | Opt-in request body capture |
109
+ | `deduplication_window` | `30` | Suppresses identical events within this many seconds; set to `0` to disable |
110
+ | `max_workers` | `2` | Number of background HTTP workers |
111
+
112
+ Log levels are threshold-based: `info` accepts all levels, `warning` accepts warnings and errors, and `error` accepts errors only.
113
+
114
+ ## Reliability and privacy
115
+
116
+ - Network requests run in a background thread pool.
117
+ - HTTP failures, serialization errors, and worker failures are contained and do not propagate into application code.
118
+ - `flush()` waits for submitted reports; `shutdown()` flushes and closes the client.
119
+ - Duplicate reports with the same event fingerprint are suppressed during the configured deduplication window.
120
+ - Authorization, cookies, passwords, tokens, secrets, and API-key-like fields are redacted.
121
+ - Request bodies are not captured by default. Enable `CAPTURE_REQUEST_BODY` only after reviewing the data your application sends.
122
+ - The SDK does not intentionally collect local variables or environment variables.
123
+
124
+ ## Lifecycle
125
+
126
+ ```python
127
+ bidar_sdk.flush()
128
+ bidar_sdk.shutdown()
129
+ ```
130
+
131
+ Call `flush()` during controlled shutdown if pending reports should be given a chance to finish. `shutdown()` prevents further submissions until the SDK is initialized again.
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,23 @@
1
+ from .client import (
2
+ BidarClient,
3
+ capture_event,
4
+ capture_exception,
5
+ capture_message,
6
+ flush,
7
+ get_client,
8
+ init,
9
+ shutdown,
10
+ )
11
+
12
+ __all__ = [
13
+ "BidarClient",
14
+ "init",
15
+ "get_client",
16
+ "capture_exception",
17
+ "capture_message",
18
+ "capture_event",
19
+ "flush",
20
+ "shutdown",
21
+ ]
22
+
23
+ __version__ = "0.2.0"
@@ -0,0 +1,241 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import platform
5
+ import sys
6
+ import threading
7
+ import traceback
8
+ from concurrent.futures import ThreadPoolExecutor, wait
9
+ from datetime import datetime, timezone
10
+ from time import monotonic
11
+
12
+ from .config import Config
13
+ from .transport import Transport
14
+ from .utils.sanitizers import redact_mapping
15
+
16
+ logger = logging.getLogger("bidar")
17
+ _LEVELS = {"info": 10, "warning": 20, "error": 30}
18
+ _client = None
19
+ _client_lock = threading.RLock()
20
+
21
+
22
+ class BidarClient:
23
+ def __init__(self, config):
24
+ self.config = config
25
+ self.transport = Transport(config)
26
+ self.executor = ThreadPoolExecutor(
27
+ max_workers=config.max_workers,
28
+ thread_name_prefix="bidar-sdk",
29
+ )
30
+ self._futures = set()
31
+ self._lock = threading.RLock()
32
+ self._recent_events = {}
33
+ self._closed = False
34
+
35
+ def _enabled(self, level):
36
+ level = level.lower()
37
+ if level not in _LEVELS:
38
+ raise ValueError("level must be info, warning, or error")
39
+ return self.config.enabled and _LEVELS[level] >= _LEVELS[self.config.log_level]
40
+
41
+ def _fingerprint(self, payload, level):
42
+ stable = json.dumps(
43
+ {"level": level, "payload": payload},
44
+ sort_keys=True,
45
+ default=str,
46
+ separators=(",", ":"),
47
+ )
48
+ return hashlib.sha256(stable.encode("utf-8")).hexdigest()
49
+
50
+ def _is_duplicate(self, fingerprint):
51
+ window = self.config.deduplication_window
52
+ if window <= 0:
53
+ return False
54
+ now = monotonic()
55
+ with self._lock:
56
+ expired = [
57
+ key for key, timestamp in self._recent_events.items()
58
+ if now - timestamp >= window
59
+ ]
60
+ for key in expired:
61
+ self._recent_events.pop(key, None)
62
+ previous = self._recent_events.get(fingerprint)
63
+ if previous is not None and now - previous < window:
64
+ return True
65
+ self._recent_events[fingerprint] = now
66
+ return False
67
+
68
+ def _submit(self, payload):
69
+ try:
70
+ if self._closed:
71
+ return False
72
+ future = self.executor.submit(self.transport.send, payload)
73
+ with self._lock:
74
+ self._futures.add(future)
75
+ future.add_done_callback(self._discard_future)
76
+ return True
77
+ except Exception:
78
+ logger.exception("Failed to queue an event for Bidar")
79
+ return False
80
+
81
+ def _discard_future(self, future):
82
+ with self._lock:
83
+ self._futures.discard(future)
84
+ try:
85
+ future.result()
86
+ except Exception:
87
+ logger.exception("Background event submission failed")
88
+
89
+ def capture_event(self, payload, level="info"):
90
+ try:
91
+ level = level.lower()
92
+ if not self._enabled(level):
93
+ return False
94
+ safe_payload = redact_mapping(payload if isinstance(payload, dict) else {"data": payload})
95
+ fingerprint = self._fingerprint(safe_payload, level)
96
+ if self._is_duplicate(fingerprint):
97
+ return False
98
+ event = {
99
+ "level": level,
100
+ "timestamp": datetime.now(timezone.utc).isoformat(),
101
+ "project_name": self.config.project_name,
102
+ "environment": self.config.environment,
103
+ "runtime": {
104
+ "language": "python",
105
+ "python_version": platform.python_version(),
106
+ "implementation": sys.implementation.name,
107
+ },
108
+ **safe_payload,
109
+ }
110
+ return self._submit(event)
111
+ except Exception:
112
+ logger.exception("Failed to prepare a Bidar event")
113
+ return False
114
+
115
+ def capture_exception(self, error, *, request=None, extra=None):
116
+ try:
117
+ payload = {
118
+ "exception_type": type(error).__name__,
119
+ "exception_message": str(error),
120
+ "traceback": "".join(
121
+ traceback.format_exception(type(error), error, error.__traceback__)
122
+ ),
123
+ }
124
+ if request is not None:
125
+ payload["request"] = request
126
+ if extra:
127
+ payload["extra"] = redact_mapping(extra)
128
+ return self.capture_event(payload, level="error")
129
+ except Exception:
130
+ logger.exception("Failed to capture an exception")
131
+ return False
132
+
133
+ def capture_message(self, message, level="info", *, extra=None):
134
+ try:
135
+ payload = {"message": str(message)}
136
+ if extra:
137
+ payload["extra"] = redact_mapping(extra)
138
+ return self.capture_event(payload, level=level)
139
+ except Exception:
140
+ logger.exception("Failed to capture a message")
141
+ return False
142
+
143
+ def flush(self):
144
+ try:
145
+ with self._lock:
146
+ pending = list(self._futures)
147
+ if pending:
148
+ wait(pending, timeout=self.config.timeout + 1)
149
+ except Exception:
150
+ logger.exception("Failed while flushing Bidar events")
151
+
152
+ def shutdown(self):
153
+ if self._closed:
154
+ return
155
+ self.flush()
156
+ self._closed = True
157
+ try:
158
+ self.executor.shutdown(wait=False, cancel_futures=False)
159
+ except TypeError:
160
+ self.executor.shutdown(wait=False)
161
+ except Exception:
162
+ logger.exception("Failed to shut down the Bidar executor")
163
+ self.transport.close()
164
+
165
+
166
+ def init(
167
+ *,
168
+ url,
169
+ access_key,
170
+ log_level="error",
171
+ project_name=None,
172
+ environment="Production",
173
+ timeout=3.0,
174
+ enabled=True,
175
+ middleware_enabled=True,
176
+ capture_request_body=False,
177
+ deduplication_window=30.0,
178
+ max_workers=2,
179
+ ):
180
+ global _client
181
+ config = Config(
182
+ url=url,
183
+ access_key=access_key,
184
+ log_level=log_level,
185
+ project_name=project_name,
186
+ environment=environment,
187
+ timeout=timeout,
188
+ enabled=enabled,
189
+ middleware_enabled=middleware_enabled,
190
+ capture_request_body=capture_request_body,
191
+ deduplication_window=deduplication_window,
192
+ max_workers=max_workers,
193
+ )
194
+ new_client = BidarClient(config)
195
+ with _client_lock:
196
+ old_client = _client
197
+ _client = new_client
198
+ if old_client is not None:
199
+ old_client.shutdown()
200
+ return new_client
201
+
202
+
203
+ def get_client():
204
+ with _client_lock:
205
+ return _client
206
+
207
+
208
+ def _get_client():
209
+ client = get_client()
210
+ if client is None:
211
+ logger.warning("Bidar SDK is not initialized; call bidar.init() first")
212
+ return client
213
+
214
+
215
+ def capture_exception(error, **kwargs):
216
+ client = _get_client()
217
+ return client.capture_exception(error, **kwargs) if client else False
218
+
219
+
220
+ def capture_message(message, level="info", **kwargs):
221
+ client = _get_client()
222
+ return client.capture_message(message, level=level, **kwargs) if client else False
223
+
224
+
225
+ def capture_event(payload, level="info"):
226
+ client = _get_client()
227
+ return client.capture_event(payload, level=level) if client else False
228
+
229
+
230
+ def flush():
231
+ client = _get_client()
232
+ return client.flush() if client else None
233
+
234
+
235
+ def shutdown():
236
+ global _client
237
+ with _client_lock:
238
+ client = _client
239
+ _client = None
240
+ if client is not None:
241
+ client.shutdown()
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+
3
+ VALID_LEVELS = {"info", "warning", "error"}
4
+
5
+
6
+ @dataclass
7
+ class Config:
8
+ url: str
9
+ access_key: str
10
+ log_level: str = "error"
11
+ project_name: str = None
12
+ environment: str = "Production"
13
+ timeout: float = 3.0
14
+ enabled: bool = True
15
+ middleware_enabled: bool = True
16
+ capture_request_body: bool = False
17
+ deduplication_window: float = 30.0
18
+ max_workers: int = 2
19
+
20
+ def __post_init__(self):
21
+ self.log_level = self.log_level.lower()
22
+ if self.log_level not in VALID_LEVELS:
23
+ raise ValueError("log_level must be one of: info, warning, error")
24
+ if not self.url:
25
+ raise ValueError("url is required")
26
+ if not self.access_key:
27
+ raise ValueError("access_key is required")
28
+ if self.timeout <= 0:
29
+ raise ValueError("timeout must be greater than zero")
30
+ if self.deduplication_window < 0:
31
+ raise ValueError("deduplication_window cannot be negative")
32
+ if self.max_workers < 1:
33
+ raise ValueError("max_workers must be at least one")
@@ -0,0 +1,80 @@
1
+ import json
2
+ import logging
3
+
4
+ from .client import capture_exception, get_client, init
5
+ from .utils.sanitizers import redact_headers, redact_mapping
6
+
7
+ logger = logging.getLogger("bidar")
8
+
9
+
10
+ def _initialize_from_django_settings():
11
+ if get_client() is not None:
12
+ return get_client()
13
+ try:
14
+ from django.conf import settings
15
+
16
+ options = getattr(settings, "BIDAR_SDK", {})
17
+ if not options or not options.get("URL") or not options.get("ACCESS_KEY"):
18
+ return None
19
+ return init(
20
+ url=options["URL"],
21
+ access_key=options["ACCESS_KEY"],
22
+ log_level=options.get("LOG_LEVEL", "error"),
23
+ project_name=options.get("PROJECT_NAME"),
24
+ environment=options.get("ENVIRONMENT", "Production"),
25
+ timeout=options.get("TIMEOUT", 3.0),
26
+ enabled=options.get("ENABLED", True),
27
+ middleware_enabled=options.get("MIDDLEWARE_ENABLED", True),
28
+ capture_request_body=options.get("CAPTURE_REQUEST_BODY", False),
29
+ deduplication_window=options.get("DEDUPLICATION_WINDOW", 30.0),
30
+ max_workers=options.get("MAX_WORKERS", 2),
31
+ )
32
+ except Exception:
33
+ logger.exception("Could not initialize Bidar SDK from Django settings")
34
+ return None
35
+
36
+
37
+ class BidarMiddleware:
38
+ def __init__(self, get_response):
39
+ self.get_response = get_response
40
+
41
+ def __call__(self, request):
42
+ try:
43
+ return self.get_response(request)
44
+ except Exception as exc:
45
+ self._report(request, exc)
46
+ raise
47
+
48
+ def _report(self, request, error):
49
+ try:
50
+ client = _initialize_from_django_settings()
51
+ if client is None:
52
+ return
53
+ if not client.config.enabled or not client.config.middleware_enabled:
54
+ return
55
+
56
+ request_data = {
57
+ "method": request.method,
58
+ "path": request.path,
59
+ "query_params": redact_mapping(request.GET.dict()),
60
+ "headers": redact_headers(request.headers),
61
+ }
62
+
63
+ if client.config.capture_request_body:
64
+ try:
65
+ raw_body = request.body
66
+ try:
67
+ parsed_body = json.loads(raw_body.decode("utf-8"))
68
+ request_data["input"] = redact_mapping(parsed_body)
69
+ except (UnicodeDecodeError, json.JSONDecodeError):
70
+ request_data["input"] = raw_body.decode("utf-8", errors="replace")[:10000]
71
+ except Exception:
72
+ request_data["input"] = "[UNAVAILABLE]"
73
+
74
+ user = getattr(request, "user", None)
75
+ if user is not None and getattr(user, "is_authenticated", False):
76
+ request_data["user_id"] = str(user.pk)
77
+
78
+ capture_exception(error, request=request_data)
79
+ except Exception:
80
+ logger.exception("Bidar middleware failed while reporting an exception")
@@ -0,0 +1,33 @@
1
+ import logging
2
+ import requests
3
+
4
+ logger = logging.getLogger("bidar")
5
+
6
+
7
+ class Transport:
8
+ def __init__(self, config):
9
+ self.config = config
10
+ self.session = requests.Session()
11
+
12
+ def send(self, payload):
13
+ try:
14
+ response = self.session.post(
15
+ self.config.url,
16
+ json=payload,
17
+ headers={
18
+ "Content-Type": "application/json",
19
+ "X-Bidar-Access-Key": self.config.access_key,
20
+ },
21
+ timeout=self.config.timeout,
22
+ )
23
+ response.raise_for_status()
24
+ return True
25
+ except Exception:
26
+ logger.exception("Failed to submit an event to Bidar")
27
+ return False
28
+
29
+ def close(self):
30
+ try:
31
+ self.session.close()
32
+ except Exception:
33
+ logger.exception("Failed to close the Bidar HTTP session")
File without changes
@@ -0,0 +1,28 @@
1
+ SENSITIVE_PARTS = (
2
+ "authorization", "cookie", "password", "passwd", "token",
3
+ "secret", "api_key", "access_key", "refresh",
4
+ )
5
+
6
+
7
+ def redact_mapping(data):
8
+ if not isinstance(data, dict):
9
+ return data
10
+ cleaned = {}
11
+ for key, value in data.items():
12
+ key_text = str(key).lower().replace("-", "_")
13
+ if any(part in key_text for part in SENSITIVE_PARTS):
14
+ cleaned[key] = "[REDACTED]"
15
+ elif isinstance(value, dict):
16
+ cleaned[key] = redact_mapping(value)
17
+ elif isinstance(value, list):
18
+ cleaned[key] = [
19
+ redact_mapping(item) if isinstance(item, dict) else item
20
+ for item in value
21
+ ]
22
+ else:
23
+ cleaned[key] = value
24
+ return cleaned
25
+
26
+
27
+ def redact_headers(headers):
28
+ return redact_mapping(dict(headers))
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: bidar
3
+ Version: 0.1.0
4
+ Summary: Background error reporting SDK for Python and Django
5
+ Author-email: CyberHuginn <contact@cyberhuginn.com>
6
+ Maintainer-email: CyberHuginn <contact@cyberhuginn.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://cyberhuginn.com
9
+ Project-URL: Organization, https://fardadnoavar.ir
10
+ Keywords: bidar,error-reporting,django,monitoring,sentry
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: requests>=2.28
15
+ Provides-Extra: django
16
+ Requires-Dist: Django>=3.2; extra == "django"
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7; extra == "dev"
19
+ Requires-Dist: pytest-mock>=3; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # bidar-sdk-python
23
+
24
+ A lightweight error and event reporting SDK for Python applications, with optional Django integration. Reports are submitted in the background so application request handling does not wait for the remote API.
25
+
26
+ **Author:** CyberHuginn
27
+ **Organization:** Fardad Noavar
28
+ **Website:** [cyberhuginn.com](https://cyberhuginn.com)
29
+ **Organization website:** [fardadnoavar.ir](https://fardadnoavar.ir)
30
+ **Contact:** contact@cyberhuginn.com
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install bidar-sdk-python
36
+ ```
37
+
38
+ For Django projects:
39
+
40
+ ```bash
41
+ pip install "bidar-sdk-python[django]"
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ import bidar
48
+
49
+ bidar.init(
50
+ url="https://api.bidarhq.ir/api/v1/errors/",
51
+ access_key="YOUR_ACCESS_KEY",
52
+ project_name="billing-api",
53
+ environment="Production",
54
+ log_level="error",
55
+ timeout=3.0,
56
+ )
57
+
58
+ try:
59
+ run_job()
60
+ except Exception as exc:
61
+ bidar.capture_exception(exc)
62
+ ```
63
+
64
+ Reports are queued on a background worker. `capture_exception`, `capture_message`, and `capture_event` return immediately with a boolean indicating whether the event was accepted for processing.
65
+
66
+ ## Django integration
67
+
68
+ Add the middleware to `settings.py`:
69
+
70
+ ```python
71
+ MIDDLEWARE = [
72
+ # ...
73
+ "bidar.middleware.BidarMiddleware",
74
+ ]
75
+ ```
76
+
77
+ Configure the SDK:
78
+
79
+ ```python
80
+ BIDAR_SDK = {
81
+ "URL": "https://api.bidarhq.ir/api/v1/errors/",
82
+ "ACCESS_KEY": "YOUR_ACCESS_KEY",
83
+ "PROJECT_NAME": "my-django-app",
84
+ "ENVIRONMENT": "Production",
85
+ "LOG_LEVEL": "error",
86
+ "TIMEOUT": 3.0,
87
+ "ENABLED": True,
88
+ "MIDDLEWARE_ENABLED": True,
89
+ "CAPTURE_REQUEST_BODY": False,
90
+ "DEDUPLICATION_WINDOW": 30,
91
+ "MAX_WORKERS": 2,
92
+ }
93
+ ```
94
+
95
+ The middleware reports unhandled exceptions and re-raises the original exception. Set `MIDDLEWARE_ENABLED` to `False` to disable automatic middleware reporting without removing the middleware from `MIDDLEWARE`.
96
+
97
+ The SDK is initialized lazily from `BIDAR_SDK` when the middleware first runs. You can also call `bidar_sdk.init(...)` explicitly during application startup.
98
+
99
+ ## Manual reporting
100
+
101
+ ```python
102
+ import bidar
103
+
104
+ bidar.capture_exception(error)
105
+
106
+ bidar.capture_message(
107
+ "Payment provider is unavailable",
108
+ level="warning",
109
+ )
110
+
111
+ bidar.capture_event(
112
+ {"event": "subscription.renewal_failed", "customer_id": 42},
113
+ level="error",
114
+ )
115
+ ```
116
+
117
+ ## Configuration
118
+
119
+ | Option | Default | Description |
120
+ |---|---:|---|
121
+ | `url` | Required | HTTP endpoint that receives JSON POST reports |
122
+ | `access_key` | Required | API key sent in `X-Bidar-Access-Key` |
123
+ | `log_level` | `error` | One of `info`, `warning`, or `error` |
124
+ | `project_name` | `None` | Project identifier attached to every event |
125
+ | `environment` | `Production` | Deployment environment |
126
+ | `timeout` | `3.0` | Outbound HTTP timeout in seconds |
127
+ | `enabled` | `True` | Enables or disables all reporting |
128
+ | `middleware_enabled` | `True` | Enables or disables Django middleware reporting |
129
+ | `capture_request_body` | `False` | Opt-in request body capture |
130
+ | `deduplication_window` | `30` | Suppresses identical events within this many seconds; set to `0` to disable |
131
+ | `max_workers` | `2` | Number of background HTTP workers |
132
+
133
+ Log levels are threshold-based: `info` accepts all levels, `warning` accepts warnings and errors, and `error` accepts errors only.
134
+
135
+ ## Reliability and privacy
136
+
137
+ - Network requests run in a background thread pool.
138
+ - HTTP failures, serialization errors, and worker failures are contained and do not propagate into application code.
139
+ - `flush()` waits for submitted reports; `shutdown()` flushes and closes the client.
140
+ - Duplicate reports with the same event fingerprint are suppressed during the configured deduplication window.
141
+ - Authorization, cookies, passwords, tokens, secrets, and API-key-like fields are redacted.
142
+ - Request bodies are not captured by default. Enable `CAPTURE_REQUEST_BODY` only after reviewing the data your application sends.
143
+ - The SDK does not intentionally collect local variables or environment variables.
144
+
145
+ ## Lifecycle
146
+
147
+ ```python
148
+ bidar_sdk.flush()
149
+ bidar_sdk.shutdown()
150
+ ```
151
+
152
+ Call `flush()` during controlled shutdown if pending reports should be given a chance to finish. `shutdown()` prevents further submissions until the SDK is initialized again.
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ bidar/__init__.py
5
+ bidar/client.py
6
+ bidar/config.py
7
+ bidar/middleware.py
8
+ bidar/transport.py
9
+ bidar.egg-info/PKG-INFO
10
+ bidar.egg-info/SOURCES.txt
11
+ bidar.egg-info/dependency_links.txt
12
+ bidar.egg-info/requires.txt
13
+ bidar.egg-info/top_level.txt
14
+ bidar/utils/__init__.py
15
+ bidar/utils/sanitizers.py
16
+ tests/test_client.py
17
+ tests/test_config.py
18
+ tests/test_sanitizers.py
@@ -0,0 +1,8 @@
1
+ requests>=2.28
2
+
3
+ [dev]
4
+ pytest>=7
5
+ pytest-mock>=3
6
+
7
+ [django]
8
+ Django>=3.2
@@ -0,0 +1 @@
1
+ bidar
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bidar"
7
+ version = "0.1.0"
8
+ description = "Background error reporting SDK for Python and Django"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "CyberHuginn", email = "contact@cyberhuginn.com" }]
13
+ maintainers = [{ name = "CyberHuginn", email = "contact@cyberhuginn.com" }]
14
+ keywords = ["bidar", "error-reporting", "django", "monitoring", "sentry"]
15
+ dependencies = ["requests>=2.28"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://cyberhuginn.com"
19
+ Organization = "https://fardadnoavar.ir"
20
+
21
+ [project.optional-dependencies]
22
+ django = ["Django>=3.2"]
23
+ dev = ["pytest>=7", "pytest-mock>=3"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ include = ["bidar*"]
bidar-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,58 @@
1
+ from bidar.client import BidarClient
2
+ from bidar.config import Config
3
+
4
+
5
+ class FakeTransport:
6
+ def __init__(self, config):
7
+ self.config = config
8
+ self.sent = []
9
+
10
+ def send(self, payload):
11
+ self.sent.append(payload)
12
+ return True
13
+
14
+ def close(self):
15
+ pass
16
+
17
+
18
+ def make_client():
19
+ client = BidarClient(Config(
20
+ url="https://example.com",
21
+ access_key="test-key",
22
+ project_name="test-project",
23
+ environment="Development",
24
+ deduplication_window=10,
25
+ ))
26
+ client.transport.close()
27
+ client.transport = FakeTransport(client.config)
28
+ return client
29
+
30
+
31
+ def test_event_contains_project_and_environment():
32
+ client = make_client()
33
+ assert client.capture_message("hello", level="info")
34
+ client.flush()
35
+ assert client.transport.sent[0]["project_name"] == "test-project"
36
+ assert client.transport.sent[0]["environment"] == "Development"
37
+ client.shutdown()
38
+
39
+
40
+ def test_duplicate_events_are_suppressed():
41
+ client = make_client()
42
+ assert client.capture_message("same event", level="error")
43
+ assert client.capture_message("same event", level="error") is False
44
+ client.flush()
45
+ assert len(client.transport.sent) == 1
46
+ client.shutdown()
47
+
48
+
49
+ def test_transport_failure_does_not_escape_capture():
50
+ client = make_client()
51
+
52
+ def broken_send(payload):
53
+ raise RuntimeError("network failure")
54
+
55
+ client.transport.send = broken_send
56
+ assert client.capture_message("failure", level="error")
57
+ client.flush()
58
+ client.shutdown()
@@ -0,0 +1,13 @@
1
+ import pytest
2
+
3
+ from bidar.config import Config
4
+
5
+
6
+ def test_config_rejects_invalid_log_level():
7
+ with pytest.raises(ValueError):
8
+ Config(url="https://example.com", access_key="key", log_level="debug")
9
+
10
+
11
+ def test_config_accepts_supported_levels():
12
+ for level in ("info", "warning", "error"):
13
+ assert Config(url="https://example.com", access_key="key", log_level=level)
@@ -0,0 +1,13 @@
1
+ from bidar.utils.sanitizers import redact_mapping
2
+
3
+
4
+ def test_redacts_sensitive_fields_recursively():
5
+ result = redact_mapping({
6
+ "password": "secret",
7
+ "nested": {"access_key": "abc", "name": "Alice"},
8
+ "items": [{"refresh_token": "xyz"}],
9
+ })
10
+ assert result["password"] == "[REDACTED]"
11
+ assert result["nested"]["access_key"] == "[REDACTED]"
12
+ assert result["nested"]["name"] == "Alice"
13
+ assert result["items"][0]["refresh_token"] == "[REDACTED]"