xecurecode-python-sdk 0.1.0__py3-none-any.whl
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.
- xecurecode/__init__.py +17 -0
- xecurecode/client.py +223 -0
- xecurecode/config.py +22 -0
- xecurecode/django_integration.py +71 -0
- xecurecode/fastapi_integration.py +36 -0
- xecurecode/flask_integration.py +55 -0
- xecurecode/payload.py +148 -0
- xecurecode_python_sdk-0.1.0.dist-info/METADATA +243 -0
- xecurecode_python_sdk-0.1.0.dist-info/RECORD +11 -0
- xecurecode_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- xecurecode_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
xecurecode/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XecureCode SDK for Python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .config import ReliabilityConfig
|
|
6
|
+
from .client import ReliabilityClient, init, get_client
|
|
7
|
+
from .payload import create_error_payload
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ReliabilityConfig",
|
|
13
|
+
"ReliabilityClient",
|
|
14
|
+
"create_error_payload",
|
|
15
|
+
"init",
|
|
16
|
+
"get_client",
|
|
17
|
+
]
|
xecurecode/client.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reliability Client for Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
from urllib.request import Request, urlopen
|
|
15
|
+
from urllib.error import URLError
|
|
16
|
+
|
|
17
|
+
from .config import ReliabilityConfig
|
|
18
|
+
from .payload import create_error_payload
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
DEFAULT_ENDPOINT = "https://xecurecode-backend.agreeablemeadow-24a6afd4.centralindia.azurecontainerapps.io/api/v1/ingest"
|
|
23
|
+
|
|
24
|
+
DEDUP_WINDOW_MS = 60000
|
|
25
|
+
MIN_INTERVAL_MS = 100
|
|
26
|
+
MAX_PENDING = 100
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReliabilityClient:
|
|
30
|
+
"""Main reliability client for capturing and sending errors"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: ReliabilityConfig):
|
|
33
|
+
self.config = config
|
|
34
|
+
self._executor = ThreadPoolExecutor(
|
|
35
|
+
max_workers=1, thread_name_prefix="reliability-sender"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Rate limiting
|
|
39
|
+
self._last_sent_time = 0
|
|
40
|
+
self._pending_sends = 0
|
|
41
|
+
self._lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
# Deduplication cache
|
|
44
|
+
self._error_cache: Dict[str, float] = {}
|
|
45
|
+
self._cache_cleanup_interval = 60
|
|
46
|
+
self._start_cache_cleanup()
|
|
47
|
+
|
|
48
|
+
# Global exception handlers
|
|
49
|
+
self._setup_global_handlers()
|
|
50
|
+
|
|
51
|
+
logger.info(f"ReliabilityClient initialized for service: {config.service_id}")
|
|
52
|
+
|
|
53
|
+
def _setup_global_handlers(self):
|
|
54
|
+
"""Setup global exception handlers"""
|
|
55
|
+
# Store original handlers
|
|
56
|
+
self._original_excepthook = sys.excepthook
|
|
57
|
+
self._original_thread_excepthook = threading.excepthook
|
|
58
|
+
|
|
59
|
+
# Set custom handlers
|
|
60
|
+
sys.excepthook = self._custom_excepthook
|
|
61
|
+
threading.excepthook = self._custom_thread_excepthook
|
|
62
|
+
|
|
63
|
+
def _custom_excepthook(self, exc_type, exc_value, exc_traceback):
|
|
64
|
+
"""Custom uncaught exception handler"""
|
|
65
|
+
if issubclass(exc_type, Exception):
|
|
66
|
+
self.capture(exc_value)
|
|
67
|
+
# Call original handler
|
|
68
|
+
if self._original_excepthook:
|
|
69
|
+
self._original_excepthook(exc_type, exc_value, exc_traceback)
|
|
70
|
+
|
|
71
|
+
def _custom_thread_excepthook(self, args):
|
|
72
|
+
"""Custom thread exception handler"""
|
|
73
|
+
if args.exc_type and issubclass(args.exc_type, Exception):
|
|
74
|
+
self.capture(args.exc_value)
|
|
75
|
+
if self._original_thread_excepthook:
|
|
76
|
+
self._original_thread_excepthook(args)
|
|
77
|
+
|
|
78
|
+
def _start_cache_cleanup(self):
|
|
79
|
+
"""Start background cache cleanup"""
|
|
80
|
+
|
|
81
|
+
def cleanup():
|
|
82
|
+
while True:
|
|
83
|
+
time.sleep(self._cache_cleanup_interval)
|
|
84
|
+
self._cleanup_cache()
|
|
85
|
+
|
|
86
|
+
cleanup_thread = threading.Thread(target=cleanup, daemon=True)
|
|
87
|
+
cleanup_thread.start()
|
|
88
|
+
|
|
89
|
+
def _cleanup_cache(self):
|
|
90
|
+
"""Remove old entries from cache"""
|
|
91
|
+
now = time.time() * 1000
|
|
92
|
+
expired_keys = [
|
|
93
|
+
k for k, v in self._error_cache.items() if now - v > DEDUP_WINDOW_MS
|
|
94
|
+
]
|
|
95
|
+
for k in expired_keys:
|
|
96
|
+
self._error_cache.pop(k, None)
|
|
97
|
+
|
|
98
|
+
def _is_duplicate(self, fingerprint: str) -> bool:
|
|
99
|
+
"""Check if error is duplicate"""
|
|
100
|
+
now = time.time() * 1000
|
|
101
|
+
last_time = self._error_cache.get(fingerprint)
|
|
102
|
+
|
|
103
|
+
if last_time is None:
|
|
104
|
+
self._error_cache[fingerprint] = now
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
if now - last_time > DEDUP_WINDOW_MS:
|
|
108
|
+
self._error_cache[fingerprint] = now
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
def _can_send(self) -> bool:
|
|
114
|
+
"""Check if we can send (rate limiting)"""
|
|
115
|
+
now = time.time() * 1000
|
|
116
|
+
with self._lock:
|
|
117
|
+
if now - self._last_sent_time < MIN_INTERVAL_MS:
|
|
118
|
+
return False
|
|
119
|
+
if self._pending_sends >= MAX_PENDING:
|
|
120
|
+
return False
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def capture(self, error: Exception, request: Any = None):
|
|
124
|
+
"""Capture an error"""
|
|
125
|
+
if error is None:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
payload = create_error_payload(
|
|
129
|
+
error, self.config.service_id, self.config.mode, request
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if self._is_duplicate(payload["fingerprint"]):
|
|
133
|
+
logger.debug(f"Duplicate error, skipping: {error}")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if not self._can_send():
|
|
137
|
+
logger.debug(f"Rate limited, skipping: {error}")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
with self._lock:
|
|
141
|
+
self._pending_sends += 1
|
|
142
|
+
self._last_sent_time = time.time() * 1000
|
|
143
|
+
|
|
144
|
+
# Send in background
|
|
145
|
+
self._executor.submit(self._send_to_backend, payload)
|
|
146
|
+
|
|
147
|
+
def _send_to_backend(self, payload: Dict[str, Any]):
|
|
148
|
+
"""Send payload to backend with retry"""
|
|
149
|
+
max_retries = 3
|
|
150
|
+
retry_delay = 1
|
|
151
|
+
try:
|
|
152
|
+
for attempt in range(max_retries):
|
|
153
|
+
try:
|
|
154
|
+
self._do_send(payload)
|
|
155
|
+
logger.debug(f"Error sent successfully: {payload['message']}")
|
|
156
|
+
break
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.warning(f"Failed to send error (attempt {attempt + 1}): {e}")
|
|
159
|
+
if attempt < max_retries - 1:
|
|
160
|
+
time.sleep(retry_delay * (attempt + 1))
|
|
161
|
+
finally:
|
|
162
|
+
with self._lock:
|
|
163
|
+
self._pending_sends -= 1
|
|
164
|
+
|
|
165
|
+
def _do_send(self, payload: Dict[str, Any]):
|
|
166
|
+
"""Perform actual HTTP request"""
|
|
167
|
+
data = json.dumps(payload).encode("utf-8")
|
|
168
|
+
|
|
169
|
+
request = Request(
|
|
170
|
+
DEFAULT_ENDPOINT,
|
|
171
|
+
data=data,
|
|
172
|
+
headers={
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
"x-api-key": self.config.api_key,
|
|
175
|
+
},
|
|
176
|
+
method="POST",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
with urlopen(request, timeout=5) as response:
|
|
180
|
+
if response.status >= 400:
|
|
181
|
+
raise Exception(f"HTTP {response.status}")
|
|
182
|
+
|
|
183
|
+
def middleware(self):
|
|
184
|
+
"""Create middleware for frameworks"""
|
|
185
|
+
return ReliabilityMiddleware(self)
|
|
186
|
+
|
|
187
|
+
async def flush(self):
|
|
188
|
+
"""Flush pending sends"""
|
|
189
|
+
while self._pending_sends > 0:
|
|
190
|
+
time.sleep(0.1)
|
|
191
|
+
|
|
192
|
+
def shutdown(self):
|
|
193
|
+
"""Shutdown the client"""
|
|
194
|
+
self._executor.shutdown(wait=True)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ReliabilityMiddleware:
|
|
198
|
+
"""Middleware for FastAPI/Flask"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, client: ReliabilityClient):
|
|
201
|
+
self.client = client
|
|
202
|
+
|
|
203
|
+
async def __call__(self, request, call_next):
|
|
204
|
+
"""Process request"""
|
|
205
|
+
try:
|
|
206
|
+
response = await call_next(request)
|
|
207
|
+
return response
|
|
208
|
+
except Exception as e:
|
|
209
|
+
self.client.capture(e, request)
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_client() -> ReliabilityClient:
|
|
214
|
+
"""Get global client instance"""
|
|
215
|
+
if not hasattr(get_client, "_instance"):
|
|
216
|
+
raise RuntimeError("ReliabilityClient not initialized. Call init() first.")
|
|
217
|
+
return get_client._instance
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def init(config: ReliabilityConfig) -> ReliabilityClient:
|
|
221
|
+
"""Initialize global client"""
|
|
222
|
+
get_client._instance = ReliabilityClient(config)
|
|
223
|
+
return get_client._instance
|
xecurecode/config.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reliability Config for Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ReliabilityConfig:
|
|
11
|
+
api_key: str
|
|
12
|
+
service_id: str
|
|
13
|
+
mode: str = "development"
|
|
14
|
+
timeout: int = 5000
|
|
15
|
+
|
|
16
|
+
def __post_init__(self):
|
|
17
|
+
if not self.api_key:
|
|
18
|
+
raise ValueError("API Key is required")
|
|
19
|
+
if not self.service_id:
|
|
20
|
+
raise ValueError("Service ID is required")
|
|
21
|
+
if self.mode not in ["development", "production"]:
|
|
22
|
+
raise ValueError("Mode must be 'development' or 'production'")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# To use with Django, add this to your settings.py:
|
|
6
|
+
#
|
|
7
|
+
# RELIABILITY_API_KEY = 'your-api-key'
|
|
8
|
+
# RELIABILITY_SERVICE_ID = 'your-service'
|
|
9
|
+
# RELIABILITY_MODE = 'development'
|
|
10
|
+
#
|
|
11
|
+
# And add to MIDDLEWARE:
|
|
12
|
+
# 'reliability.django_integration.ReliabilityMiddleware'
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from django.core.exceptions import MiddlewareMixin
|
|
16
|
+
|
|
17
|
+
from ..client import ReliabilityClient
|
|
18
|
+
from ..config import ReliabilityConfig
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_client = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_client() -> ReliabilityClient:
|
|
26
|
+
"""Get or create Django client"""
|
|
27
|
+
global _client
|
|
28
|
+
if _client is None:
|
|
29
|
+
from django.conf import settings
|
|
30
|
+
|
|
31
|
+
api_key = getattr(settings, "RELIABILITY_API_KEY", None)
|
|
32
|
+
service_id = getattr(settings, "RELIABILITY_SERVICE_ID", None)
|
|
33
|
+
mode = getattr(settings, "RELIABILITY_MODE", "development")
|
|
34
|
+
|
|
35
|
+
if not api_key or not service_id:
|
|
36
|
+
logger.warning(
|
|
37
|
+
"Reliability SDK not configured. Add API_KEY and SERVICE_ID to settings."
|
|
38
|
+
)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
config = ReliabilityConfig(api_key=api_key, service_id=service_id, mode=mode)
|
|
42
|
+
_client = ReliabilityClient(config)
|
|
43
|
+
|
|
44
|
+
return _client
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReliabilityMiddleware(MiddlewareMixin):
|
|
48
|
+
"""Django middleware for error capture"""
|
|
49
|
+
|
|
50
|
+
def process_exception(self, request, exception):
|
|
51
|
+
"""Capture exceptions"""
|
|
52
|
+
client = get_client()
|
|
53
|
+
if client:
|
|
54
|
+
client.capture(exception, request)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Django signals for automatic capture
|
|
59
|
+
def setup_django_signals():
|
|
60
|
+
"""Setup Django signals for automatic error capture"""
|
|
61
|
+
try:
|
|
62
|
+
from django.core.signals import exception_handler
|
|
63
|
+
from django.dispatch import receiver
|
|
64
|
+
|
|
65
|
+
@receiver(exception_handler)
|
|
66
|
+
def capture_exception(**kwargs):
|
|
67
|
+
client = get_client()
|
|
68
|
+
if client and "request" in kwargs:
|
|
69
|
+
client.capture(kwargs["exception"], kwargs["request"])
|
|
70
|
+
except ImportError:
|
|
71
|
+
pass
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, Request, Response
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from .client import ReliabilityClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FastAPIMiddleware(BaseHTTPMiddleware):
|
|
14
|
+
"""FastAPI middleware for error capture"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, app: FastAPI, client: ReliabilityClient):
|
|
17
|
+
super().__init__(app)
|
|
18
|
+
self.client = client
|
|
19
|
+
|
|
20
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
21
|
+
try:
|
|
22
|
+
response = await call_next(request)
|
|
23
|
+
return response
|
|
24
|
+
except Exception as e:
|
|
25
|
+
self.client.capture(e, request)
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def setup_fastapi(app: FastAPI, client: ReliabilityClient):
|
|
30
|
+
"""Setup FastAPI integration"""
|
|
31
|
+
app.add_middleware(FastAPIMiddleware, client=client)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_fastapi_middleware(client: ReliabilityClient):
|
|
35
|
+
"""Create FastAPI middleware for manual use"""
|
|
36
|
+
return FastAPIMiddleware
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from flask import Flask, Request, Response
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from .client import ReliabilityClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlaskMiddleware:
|
|
12
|
+
"""Flask middleware for error capture"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: ReliabilityClient):
|
|
15
|
+
self.client = client
|
|
16
|
+
|
|
17
|
+
def __call__(self, environ: dict, start_response: Callable) -> Response:
|
|
18
|
+
# Wrap start_response to capture exceptions
|
|
19
|
+
def wrapped_start_response(status, headers, exc_info=None):
|
|
20
|
+
return start_response(status, headers, exc_info)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# Use Flask's internal mechanism
|
|
24
|
+
from flask import g
|
|
25
|
+
|
|
26
|
+
return self.app(environ, wrapped_start_response)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
# Get request object from Flask
|
|
29
|
+
from flask import request
|
|
30
|
+
|
|
31
|
+
self.client.capture(e, request)
|
|
32
|
+
raise
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def init_flask(app: Flask, client: ReliabilityClient):
|
|
36
|
+
"""Initialize Flask integration"""
|
|
37
|
+
|
|
38
|
+
@app.errorhandler(Exception)
|
|
39
|
+
def handle_error(error):
|
|
40
|
+
from flask import request
|
|
41
|
+
|
|
42
|
+
client.capture(error, request)
|
|
43
|
+
return {"error": str(error)}, 500
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_flask_error_handler(client: ReliabilityClient):
|
|
47
|
+
"""Create Flask error handler for manual use"""
|
|
48
|
+
|
|
49
|
+
def handle_error(error):
|
|
50
|
+
from flask import request
|
|
51
|
+
|
|
52
|
+
client.capture(error, request)
|
|
53
|
+
return {"error": str(error)}, 500
|
|
54
|
+
|
|
55
|
+
return handle_error
|
xecurecode/payload.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error Payload for Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import platform
|
|
7
|
+
import os
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _generate_fingerprint(error: Exception, stack: Optional[str] = None) -> str:
|
|
13
|
+
"""Generate deterministic fingerprint for error"""
|
|
14
|
+
signature = str(error)
|
|
15
|
+
if stack:
|
|
16
|
+
first_line = stack.split("\n")[0] if stack else ""
|
|
17
|
+
signature += first_line
|
|
18
|
+
|
|
19
|
+
return hashlib.sha256(signature.encode()).hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _classify_error(error: Exception) -> str:
|
|
23
|
+
"""Classify error type based on message"""
|
|
24
|
+
msg = str(error).lower()
|
|
25
|
+
|
|
26
|
+
if any(
|
|
27
|
+
keyword in msg
|
|
28
|
+
for keyword in ["database", "sql", "psycopg", "mysql", "sqlite", "connection"]
|
|
29
|
+
):
|
|
30
|
+
return "DATABASE"
|
|
31
|
+
if any(
|
|
32
|
+
keyword in msg
|
|
33
|
+
for keyword in ["network", "http", "socket", "connection", "timeout", "ssl"]
|
|
34
|
+
):
|
|
35
|
+
return "NETWORK"
|
|
36
|
+
if any(
|
|
37
|
+
keyword in msg
|
|
38
|
+
for keyword in ["validation", "invalid", "required", "constraint", "type"]
|
|
39
|
+
):
|
|
40
|
+
return "VALIDATION"
|
|
41
|
+
if any(
|
|
42
|
+
keyword in msg
|
|
43
|
+
for keyword in ["permission", "denied", "forbidden", "unauthorized"]
|
|
44
|
+
):
|
|
45
|
+
return "AUTH"
|
|
46
|
+
|
|
47
|
+
return "RUNTIME"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _determine_severity(error: Exception) -> str:
|
|
51
|
+
"""Determine error severity"""
|
|
52
|
+
msg = str(error).lower()
|
|
53
|
+
|
|
54
|
+
if any(
|
|
55
|
+
keyword in msg
|
|
56
|
+
for keyword in ["database", "connection", "timeout", "fatal", "crash"]
|
|
57
|
+
):
|
|
58
|
+
return "critical"
|
|
59
|
+
return "warning"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _format_stack_trace(stack: Optional[str]) -> Optional[str]:
|
|
63
|
+
"""Format stack trace"""
|
|
64
|
+
if not stack:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
if isinstance(stack, str):
|
|
68
|
+
return stack
|
|
69
|
+
|
|
70
|
+
if hasattr(stack, "__traceback__"):
|
|
71
|
+
import traceback
|
|
72
|
+
|
|
73
|
+
return "".join(traceback.format_tb(stack.__traceback__))
|
|
74
|
+
|
|
75
|
+
return str(stack)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_service_context() -> Dict[str, Any]:
|
|
79
|
+
"""Build service context"""
|
|
80
|
+
return {
|
|
81
|
+
"pid": os.getpid(),
|
|
82
|
+
"python_version": platform.python_version(),
|
|
83
|
+
"platform": platform.platform(),
|
|
84
|
+
"hostname": platform.node() or os.environ.get("HOSTNAME", "unknown"),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _extract_request_context(request: Optional[Any] = None) -> Optional[Dict[str, Any]]:
|
|
89
|
+
"""Extract request context from various frameworks"""
|
|
90
|
+
if request is None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
context = {}
|
|
94
|
+
|
|
95
|
+
# FastAPI/Starlette
|
|
96
|
+
if hasattr(request, "method"):
|
|
97
|
+
context["method"] = request.method
|
|
98
|
+
|
|
99
|
+
if hasattr(request, "url"):
|
|
100
|
+
context["url"] = str(request.url)
|
|
101
|
+
|
|
102
|
+
if hasattr(request, "client") and request.client:
|
|
103
|
+
context["ip"] = request.client.host
|
|
104
|
+
|
|
105
|
+
# Flask
|
|
106
|
+
if hasattr(request, "remote_addr"):
|
|
107
|
+
context["ip"] = request.remote_addr
|
|
108
|
+
|
|
109
|
+
if hasattr(request, "path"):
|
|
110
|
+
context["url"] = context.get("url", request.path)
|
|
111
|
+
|
|
112
|
+
# Django
|
|
113
|
+
if hasattr(request, "META"):
|
|
114
|
+
context["ip"] = request.META.get("REMOTE_ADDR")
|
|
115
|
+
context["method"] = request.method
|
|
116
|
+
context["url"] = request.path
|
|
117
|
+
|
|
118
|
+
# Generic headers
|
|
119
|
+
if hasattr(request, "headers"):
|
|
120
|
+
forwarded = request.headers.get("x-forwarded-for")
|
|
121
|
+
if forwarded and not context.get("ip"):
|
|
122
|
+
context["ip"] = forwarded.split(",")[0].strip()
|
|
123
|
+
|
|
124
|
+
return context if context else None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def create_error_payload(
|
|
128
|
+
error: Exception, service_id: str, mode: str, request: Optional[Any] = None
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""Create structured error payload"""
|
|
131
|
+
|
|
132
|
+
stack = _format_stack_trace(error.__traceback__)
|
|
133
|
+
fingerprint = _generate_fingerprint(error, stack)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"message": str(error),
|
|
137
|
+
"name": type(error).__name__,
|
|
138
|
+
"stack": stack,
|
|
139
|
+
"fingerprint": fingerprint,
|
|
140
|
+
"timestamp": int(datetime.now().timestamp() * 1000),
|
|
141
|
+
"service_id": service_id,
|
|
142
|
+
"environment": mode,
|
|
143
|
+
"occurrence_count": 1,
|
|
144
|
+
"severity": _determine_severity(error),
|
|
145
|
+
"error_type": _classify_error(error),
|
|
146
|
+
"service_context": _build_service_context(),
|
|
147
|
+
"request_context": _extract_request_context(request),
|
|
148
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xecurecode-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-driven reliability SDK for Python applications
|
|
5
|
+
Author-email: Xel Team <team@xel.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
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
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: fastapi
|
|
19
|
+
Requires-Dist: fastapi>=0.100; extra == "fastapi"
|
|
20
|
+
Provides-Extra: flask
|
|
21
|
+
Requires-Dist: flask>=2.0; extra == "flask"
|
|
22
|
+
Provides-Extra: django
|
|
23
|
+
Requires-Dist: django>=3.0; extra == "django"
|
|
24
|
+
|
|
25
|
+
# XecureTrace Reliability SDK - Python
|
|
26
|
+
|
|
27
|
+
AI-driven reliability SDK for Python applications.
|
|
28
|
+
|
|
29
|
+
The official Python SDK for the XecureTrace Reliability Platform — a human-first failure analysis and recovery recommendation system.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Why This SDK Exists
|
|
34
|
+
|
|
35
|
+
Modern backend systems fail in unpredictable ways.
|
|
36
|
+
|
|
37
|
+
This SDK:
|
|
38
|
+
|
|
39
|
+
- Captures structured runtime errors
|
|
40
|
+
- Generates deterministic fingerprints
|
|
41
|
+
- Classifies error types (DATABASE, NETWORK, VALIDATION, etc.)
|
|
42
|
+
- Determines severity
|
|
43
|
+
- Sends non-blocking telemetry to your backend
|
|
44
|
+
- Never crashes your application
|
|
45
|
+
|
|
46
|
+
**AI assists. Humans decide. Nothing executes automatically.**
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.8+
|
|
53
|
+
- FastAPI, Flask, or Django (optional)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install xecurecode-python-sdk
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or install with framework support:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install xecurecode-python-sdk[fastapi,flask,django]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### 1️⃣ Initialize the SDK
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from reliability import ReliabilityClient, ReliabilityConfig
|
|
77
|
+
|
|
78
|
+
config = ReliabilityConfig(
|
|
79
|
+
api_key="your-api-key",
|
|
80
|
+
service_id="your-service",
|
|
81
|
+
mode="development"
|
|
82
|
+
)
|
|
83
|
+
client = ReliabilityClient(config)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Configuration Options:**
|
|
87
|
+
|
|
88
|
+
| Parameter | Required | Description |
|
|
89
|
+
|-----------|----------|-------------|
|
|
90
|
+
| `api_key` | Yes | Your API key |
|
|
91
|
+
| `service_id` | Yes | Service identifier |
|
|
92
|
+
| `mode` | Yes | `development` or `production` |
|
|
93
|
+
| `timeout` | No | Request timeout (default: 5000ms) |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### 2️⃣ Automatic Global Error Capture
|
|
98
|
+
|
|
99
|
+
The SDK automatically handles:
|
|
100
|
+
- Uncaught exceptions
|
|
101
|
+
- Thread exceptions
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# This will be automatically captured
|
|
105
|
+
raise ValueError("Database timeout")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 3️⃣ FastAPI Integration
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from fastapi import FastAPI
|
|
114
|
+
from reliability import ReliabilityClient, ReliabilityConfig
|
|
115
|
+
from reliability.fastapi_integration import FastAPIMiddleware
|
|
116
|
+
|
|
117
|
+
config = ReliabilityConfig(...)
|
|
118
|
+
client = ReliabilityClient(config)
|
|
119
|
+
|
|
120
|
+
app = FastAPI()
|
|
121
|
+
app.add_middleware(FastAPIMiddleware, client=client)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 4️⃣ Flask Integration
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from flask import Flask
|
|
130
|
+
from reliability import ReliabilityClient, ReliabilityConfig
|
|
131
|
+
from reliability.flask_integration import init_flask
|
|
132
|
+
|
|
133
|
+
config = ReliabilityConfig(...)
|
|
134
|
+
client = ReliabilityClient(config)
|
|
135
|
+
|
|
136
|
+
app = Flask(__name__)
|
|
137
|
+
init_flask(app, client)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 5️⃣ Django Integration
|
|
143
|
+
|
|
144
|
+
In `settings.py`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
RELIABILITY_API_KEY = "your-api-key"
|
|
148
|
+
RELIABILITY_SERVICE_ID = "your-service"
|
|
149
|
+
RELIABILITY_MODE = "development"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In `settings.py` MIDDLEWARE:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
MIDDLEWARE = [
|
|
156
|
+
...
|
|
157
|
+
'reliability.django_integration.ReliabilityMiddleware',
|
|
158
|
+
...
|
|
159
|
+
]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### 6️⃣ Manual Capture
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
try:
|
|
168
|
+
risky_operation()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
client.capture(e)
|
|
171
|
+
|
|
172
|
+
# With request context
|
|
173
|
+
client.capture(e, request)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## What Gets Sent
|
|
179
|
+
|
|
180
|
+
Example structured payload:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"message": "Database connection failed",
|
|
185
|
+
"name": "ValueError",
|
|
186
|
+
"fingerprint": "a8c39c21...",
|
|
187
|
+
"timestamp": 1700000000000,
|
|
188
|
+
"service_id": "my-service",
|
|
189
|
+
"environment": "development",
|
|
190
|
+
"severity": "critical",
|
|
191
|
+
"error_type": "DATABASE",
|
|
192
|
+
"service_context": {
|
|
193
|
+
"pid": 12345,
|
|
194
|
+
"python_version": "3.11.0",
|
|
195
|
+
"platform": "Linux-5.10.0",
|
|
196
|
+
"hostname": "server-01"
|
|
197
|
+
},
|
|
198
|
+
"request_context": {
|
|
199
|
+
"method": "GET",
|
|
200
|
+
"url": "/api/users",
|
|
201
|
+
"ip": "10.0.0.5"
|
|
202
|
+
},
|
|
203
|
+
"occurrence_count": 1
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Error Classification
|
|
210
|
+
|
|
211
|
+
| Type | Example |
|
|
212
|
+
|------|---------|
|
|
213
|
+
| DATABASE | Timeout, SQL errors, connection issues |
|
|
214
|
+
| NETWORK | HTTP failures, socket errors |
|
|
215
|
+
| VALIDATION | Invalid input, type errors |
|
|
216
|
+
| AUTH | Permission denied |
|
|
217
|
+
| RUNTIME | Standard Python errors |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Severity Detection
|
|
222
|
+
|
|
223
|
+
- **Critical** → Database, timeout, connection errors
|
|
224
|
+
- **Warning** → Validation, recoverable issues
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Design Guarantees
|
|
229
|
+
|
|
230
|
+
- Non-blocking HTTP calls
|
|
231
|
+
- Timeout protected (5s default)
|
|
232
|
+
- Retry logic (3 attempts)
|
|
233
|
+
- Deduplication (1-minute window)
|
|
234
|
+
- Rate limiting (max 100 concurrent)
|
|
235
|
+
- Never throws inside SDK
|
|
236
|
+
- Never crashes host application
|
|
237
|
+
- Works with FastAPI, Flask, Django
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
xecurecode/__init__.py,sha256=Lr91KhibWBdQiEDS5gGgtuEWLYTd91rU2aEEoDCFf8s,317
|
|
2
|
+
xecurecode/client.py,sha256=mI49C1ZB1uckghC0ArMmkJKWufzrH6_tUPnzQD9fQiA,6940
|
|
3
|
+
xecurecode/config.py,sha256=DEMBw0oEysZGpzMHi_dJ2_rimLrBijLvYJOCjXeN4Tk,566
|
|
4
|
+
xecurecode/django_integration.py,sha256=4A2qCoQR5jdOWKG2Boqi_C9kiqfCHHcSfMLUl2Ar2Io,2005
|
|
5
|
+
xecurecode/fastapi_integration.py,sha256=nmrLYQI5Blpg6qSnignzNHznpUoNcggxPZ-1KY0R5gw,1021
|
|
6
|
+
xecurecode/flask_integration.py,sha256=_ILiUkR3zBHIxkOB9sopux3yjK8f8gcJiQriDYvZQTc,1435
|
|
7
|
+
xecurecode/payload.py,sha256=t1D6One_bb8m2WYALy0oJt3LOfe2yvQMnMvL9MtPyCA,4078
|
|
8
|
+
xecurecode_python_sdk-0.1.0.dist-info/METADATA,sha256=B4IHwJZ25Jgm9-fTnU34OJWEDHNwgi64ik6TZaluA5g,4962
|
|
9
|
+
xecurecode_python_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
xecurecode_python_sdk-0.1.0.dist-info/top_level.txt,sha256=7OncyRHpCaDE2-r_4RIvGbITjwenEIapFFO8vFjkD7w,11
|
|
11
|
+
xecurecode_python_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xecurecode
|