xecurecode-reliability-sdk 0.1.1__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.
- reliability/__init__.py +17 -0
- reliability/client.py +265 -0
- reliability/config.py +33 -0
- reliability/django_integration.py +66 -0
- reliability/fastapi_integration.py +38 -0
- reliability/flask_integration.py +56 -0
- reliability/payload.py +239 -0
- reliability/py.typed +0 -0
- xecurecode_reliability_sdk-0.1.1.dist-info/METADATA +247 -0
- xecurecode_reliability_sdk-0.1.1.dist-info/RECORD +13 -0
- xecurecode_reliability_sdk-0.1.1.dist-info/WHEEL +5 -0
- xecurecode_reliability_sdk-0.1.1.dist-info/licenses/LICENSE +21 -0
- xecurecode_reliability_sdk-0.1.1.dist-info/top_level.txt +1 -0
reliability/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XecureCode Reliability 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.1"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ReliabilityConfig",
|
|
13
|
+
"ReliabilityClient",
|
|
14
|
+
"create_error_payload",
|
|
15
|
+
"init",
|
|
16
|
+
"get_client",
|
|
17
|
+
]
|
reliability/client.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reliability Client for Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
from urllib.request import Request, urlopen
|
|
14
|
+
|
|
15
|
+
from .config import ReliabilityConfig
|
|
16
|
+
from .payload import create_error_payload
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
DEFAULT_ENDPOINT = "https://api.xecurecode.in/api/v1/ingest"
|
|
21
|
+
|
|
22
|
+
DEDUP_WINDOW_MS = 60000
|
|
23
|
+
MIN_INTERVAL_MS = 100
|
|
24
|
+
MAX_PENDING = 100
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReliabilityClient:
|
|
28
|
+
"""Main reliability client for capturing and sending errors"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: ReliabilityConfig):
|
|
31
|
+
self.config = config
|
|
32
|
+
self._executor = ThreadPoolExecutor(
|
|
33
|
+
max_workers=1, thread_name_prefix="reliability-sender"
|
|
34
|
+
)
|
|
35
|
+
self._shutdown_flag = False
|
|
36
|
+
|
|
37
|
+
# Rate limiting
|
|
38
|
+
self._last_sent_time = 0
|
|
39
|
+
self._pending_sends = 0
|
|
40
|
+
self._lock = threading.Lock()
|
|
41
|
+
|
|
42
|
+
# Deduplication cache
|
|
43
|
+
self._error_cache: Dict[str, float] = {}
|
|
44
|
+
self._error_counts: Dict[str, int] = {}
|
|
45
|
+
self._cache_cleanup_interval = 60
|
|
46
|
+
self._cleanup_stop = threading.Event()
|
|
47
|
+
self._cleanup_thread: Optional[threading.Thread] = None
|
|
48
|
+
self._start_cache_cleanup()
|
|
49
|
+
|
|
50
|
+
# Global exception handlers
|
|
51
|
+
self._setup_global_handlers()
|
|
52
|
+
|
|
53
|
+
logger.info(f"ReliabilityClient initialized for service: {config.service_id}")
|
|
54
|
+
|
|
55
|
+
def _setup_global_handlers(self):
|
|
56
|
+
"""Setup global exception handlers"""
|
|
57
|
+
self._original_excepthook = sys.excepthook
|
|
58
|
+
self._original_thread_excepthook = threading.excepthook
|
|
59
|
+
|
|
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
|
+
try:
|
|
66
|
+
if issubclass(exc_type, Exception):
|
|
67
|
+
self.capture(exc_value)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
if self._original_excepthook:
|
|
71
|
+
self._original_excepthook(exc_type, exc_value, exc_traceback)
|
|
72
|
+
|
|
73
|
+
def _custom_thread_excepthook(self, args):
|
|
74
|
+
"""Custom thread exception handler"""
|
|
75
|
+
try:
|
|
76
|
+
if args.exc_type and issubclass(args.exc_type, Exception):
|
|
77
|
+
self.capture(args.exc_value)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
if self._original_thread_excepthook:
|
|
81
|
+
self._original_thread_excepthook(args)
|
|
82
|
+
|
|
83
|
+
def _start_cache_cleanup(self):
|
|
84
|
+
"""Start background cache cleanup"""
|
|
85
|
+
|
|
86
|
+
def cleanup():
|
|
87
|
+
while not self._cleanup_stop.wait(self._cache_cleanup_interval):
|
|
88
|
+
self._cleanup_cache()
|
|
89
|
+
|
|
90
|
+
self._cleanup_thread = threading.Thread(
|
|
91
|
+
target=cleanup,
|
|
92
|
+
name="reliability-cache-cleanup",
|
|
93
|
+
daemon=True,
|
|
94
|
+
)
|
|
95
|
+
self._cleanup_thread.start()
|
|
96
|
+
|
|
97
|
+
def _cleanup_cache(self):
|
|
98
|
+
"""Remove old entries from cache"""
|
|
99
|
+
now = time.time() * 1000
|
|
100
|
+
expired_keys = [
|
|
101
|
+
k for k, v in self._error_cache.items() if now - v > DEDUP_WINDOW_MS
|
|
102
|
+
]
|
|
103
|
+
for k in expired_keys:
|
|
104
|
+
self._error_cache.pop(k, None)
|
|
105
|
+
self._error_counts.pop(k, None)
|
|
106
|
+
|
|
107
|
+
def _record_fingerprint(self, fingerprint: str) -> int:
|
|
108
|
+
"""Record a fingerprint occurrence and return the accumulated count."""
|
|
109
|
+
now = time.time() * 1000
|
|
110
|
+
last_time = self._error_cache.get(fingerprint)
|
|
111
|
+
current_count = self._error_counts.get(fingerprint, 0)
|
|
112
|
+
|
|
113
|
+
if last_time is None:
|
|
114
|
+
self._error_cache[fingerprint] = now
|
|
115
|
+
self._error_counts[fingerprint] = 1
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
if now - last_time > DEDUP_WINDOW_MS:
|
|
119
|
+
self._error_cache[fingerprint] = now
|
|
120
|
+
self._error_counts[fingerprint] = 1
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
self._error_cache[fingerprint] = now
|
|
124
|
+
current_count += 1
|
|
125
|
+
self._error_counts[fingerprint] = current_count
|
|
126
|
+
return current_count
|
|
127
|
+
|
|
128
|
+
def _can_send(self) -> bool:
|
|
129
|
+
"""Check if we can send (rate limiting)"""
|
|
130
|
+
now = time.time() * 1000
|
|
131
|
+
with self._lock:
|
|
132
|
+
if now - self._last_sent_time < MIN_INTERVAL_MS:
|
|
133
|
+
return False
|
|
134
|
+
if self._pending_sends >= MAX_PENDING:
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def capture(self, error: Exception, request: Any = None):
|
|
139
|
+
"""Capture an error - never throws"""
|
|
140
|
+
try:
|
|
141
|
+
if error is None:
|
|
142
|
+
return
|
|
143
|
+
if self._shutdown_flag:
|
|
144
|
+
logger.debug("Client shut down, dropping event")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
payload = create_error_payload(
|
|
148
|
+
error,
|
|
149
|
+
self.config.service_id,
|
|
150
|
+
self.config.mode,
|
|
151
|
+
request,
|
|
152
|
+
version=self.config.version,
|
|
153
|
+
release=self.config.release,
|
|
154
|
+
commit_hash=self.config.commit_hash,
|
|
155
|
+
branch=self.config.branch,
|
|
156
|
+
build_id=self.config.build_id,
|
|
157
|
+
deployment_id=self.config.deployment_id,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
occurrence_count = self._record_fingerprint(payload["fingerprint"])
|
|
161
|
+
|
|
162
|
+
if not self._can_send():
|
|
163
|
+
logger.debug(f"Rate limited, skipping: {error}")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
with self._lock:
|
|
167
|
+
self._pending_sends += 1
|
|
168
|
+
self._last_sent_time = time.time() * 1000
|
|
169
|
+
|
|
170
|
+
payload["occurrenceCount"] = occurrence_count
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
self._executor.submit(self._send_to_backend, payload)
|
|
174
|
+
except RuntimeError:
|
|
175
|
+
with self._lock:
|
|
176
|
+
self._pending_sends -= 1
|
|
177
|
+
logger.debug("Executor unavailable, dropping event")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.exception(f"Reliability SDK internal error in capture(): {e}")
|
|
180
|
+
|
|
181
|
+
def _send_to_backend(self, payload: Dict[str, Any]):
|
|
182
|
+
"""Send payload to backend with retry"""
|
|
183
|
+
max_retries = 3
|
|
184
|
+
retry_delay = 1
|
|
185
|
+
try:
|
|
186
|
+
for attempt in range(max_retries):
|
|
187
|
+
try:
|
|
188
|
+
self._do_send(payload)
|
|
189
|
+
logger.debug(f"Error sent successfully: {payload['message']}")
|
|
190
|
+
break
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Failed to send error (attempt {attempt + 1}): {e}")
|
|
193
|
+
if attempt < max_retries - 1:
|
|
194
|
+
time.sleep(retry_delay * (attempt + 1))
|
|
195
|
+
finally:
|
|
196
|
+
with self._lock:
|
|
197
|
+
self._pending_sends -= 1
|
|
198
|
+
|
|
199
|
+
def _do_send(self, payload: Dict[str, Any]):
|
|
200
|
+
"""Perform actual HTTP request"""
|
|
201
|
+
data = json.dumps(payload).encode("utf-8")
|
|
202
|
+
endpoint = self.config.endpoint or DEFAULT_ENDPOINT
|
|
203
|
+
|
|
204
|
+
request = Request(
|
|
205
|
+
endpoint,
|
|
206
|
+
data=data,
|
|
207
|
+
headers={
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
"xc-api-key": self.config.api_key,
|
|
210
|
+
"xc-service-id": self.config.service_id,
|
|
211
|
+
},
|
|
212
|
+
method="POST",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
with urlopen(request, timeout=self.config.timeout / 1000) as response:
|
|
216
|
+
if response.status >= 400:
|
|
217
|
+
raise Exception(f"HTTP {response.status}")
|
|
218
|
+
|
|
219
|
+
def middleware(self):
|
|
220
|
+
"""Create middleware for frameworks"""
|
|
221
|
+
return ReliabilityMiddleware(self)
|
|
222
|
+
|
|
223
|
+
async def flush(self):
|
|
224
|
+
"""Flush pending sends"""
|
|
225
|
+
while self._pending_sends > 0:
|
|
226
|
+
await asyncio.sleep(0.1)
|
|
227
|
+
|
|
228
|
+
def shutdown(self):
|
|
229
|
+
"""Shutdown the client"""
|
|
230
|
+
self._shutdown_flag = True
|
|
231
|
+
if getattr(self, "_original_excepthook", None):
|
|
232
|
+
sys.excepthook = self._original_excepthook
|
|
233
|
+
if getattr(self, "_original_thread_excepthook", None):
|
|
234
|
+
threading.excepthook = self._original_thread_excepthook
|
|
235
|
+
self._cleanup_stop.set()
|
|
236
|
+
self._executor.shutdown(wait=True)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ReliabilityMiddleware:
|
|
240
|
+
"""Middleware for FastAPI/Flask"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, client: ReliabilityClient):
|
|
243
|
+
self.client = client
|
|
244
|
+
|
|
245
|
+
async def __call__(self, request, call_next):
|
|
246
|
+
"""Process request"""
|
|
247
|
+
try:
|
|
248
|
+
response = await call_next(request)
|
|
249
|
+
return response
|
|
250
|
+
except Exception as e:
|
|
251
|
+
self.client.capture(e, request)
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_client() -> ReliabilityClient:
|
|
256
|
+
"""Get global client instance"""
|
|
257
|
+
if not hasattr(get_client, "_instance"):
|
|
258
|
+
raise RuntimeError("ReliabilityClient not initialized. Call init() first.")
|
|
259
|
+
return get_client._instance
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def init(config: ReliabilityConfig) -> ReliabilityClient:
|
|
263
|
+
"""Initialize global client"""
|
|
264
|
+
get_client._instance = ReliabilityClient(config)
|
|
265
|
+
return get_client._instance
|
reliability/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reliability Config for Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
Mode = Literal["development", "production"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ReliabilityConfig:
|
|
13
|
+
api_key: str
|
|
14
|
+
service_id: str
|
|
15
|
+
mode: Mode = "development"
|
|
16
|
+
timeout: int = 5000
|
|
17
|
+
endpoint: Optional[str] = None
|
|
18
|
+
version: Optional[str] = None
|
|
19
|
+
release: Optional[str] = None
|
|
20
|
+
commit_hash: Optional[str] = None
|
|
21
|
+
branch: Optional[str] = None
|
|
22
|
+
build_id: Optional[str] = None
|
|
23
|
+
deployment_id: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
if not self.api_key:
|
|
27
|
+
raise ValueError("API Key is required")
|
|
28
|
+
if not self.service_id:
|
|
29
|
+
raise ValueError("Service ID is required")
|
|
30
|
+
if self.mode not in ["development", "production"]:
|
|
31
|
+
raise ValueError("Mode must be 'development' or 'production'")
|
|
32
|
+
if self.timeout <= 0:
|
|
33
|
+
raise ValueError("Timeout must be greater than 0")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
10
|
+
|
|
11
|
+
from .client import ReliabilityClient
|
|
12
|
+
from .config import ReliabilityConfig
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_client: Optional[ReliabilityClient] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_client() -> Optional[ReliabilityClient]:
|
|
20
|
+
"""Get or create Django client"""
|
|
21
|
+
global _client
|
|
22
|
+
if _client is None:
|
|
23
|
+
from django.conf import settings
|
|
24
|
+
|
|
25
|
+
api_key = getattr(settings, "RELIABILITY_API_KEY", None)
|
|
26
|
+
service_id = getattr(settings, "RELIABILITY_SERVICE_ID", None)
|
|
27
|
+
mode = getattr(settings, "RELIABILITY_MODE", "development")
|
|
28
|
+
|
|
29
|
+
if not api_key or not service_id:
|
|
30
|
+
logger.warning(
|
|
31
|
+
"Reliability SDK not configured. Add API_KEY and SERVICE_ID to settings."
|
|
32
|
+
)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
config = ReliabilityConfig(api_key=api_key, service_id=service_id, mode=mode)
|
|
36
|
+
_client = ReliabilityClient(config)
|
|
37
|
+
|
|
38
|
+
return _client
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ReliabilityMiddleware(MiddlewareMixin):
|
|
42
|
+
"""Django middleware for error capture"""
|
|
43
|
+
|
|
44
|
+
def process_exception(self, request, exception):
|
|
45
|
+
"""Capture exceptions"""
|
|
46
|
+
client = get_client()
|
|
47
|
+
if client:
|
|
48
|
+
client.capture(exception, request)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Django signals for automatic capture
|
|
53
|
+
def setup_django_signals():
|
|
54
|
+
"""Setup Django signals for automatic error capture"""
|
|
55
|
+
try:
|
|
56
|
+
from django.core.signals import got_request_exception
|
|
57
|
+
from django.dispatch import receiver
|
|
58
|
+
|
|
59
|
+
@receiver(got_request_exception)
|
|
60
|
+
def capture_exception(sender, request=None, **kwargs):
|
|
61
|
+
client = get_client()
|
|
62
|
+
exception = sys.exc_info()[1]
|
|
63
|
+
if client and exception:
|
|
64
|
+
client.capture(exception, request)
|
|
65
|
+
except ImportError:
|
|
66
|
+
pass
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, Request, Response
|
|
6
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
7
|
+
from typing import Callable, Type
|
|
8
|
+
|
|
9
|
+
from .client import ReliabilityClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FastAPIMiddleware(BaseHTTPMiddleware):
|
|
13
|
+
"""FastAPI middleware for error capture"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, app: FastAPI, client: ReliabilityClient):
|
|
16
|
+
super().__init__(app)
|
|
17
|
+
self.client = client
|
|
18
|
+
|
|
19
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
20
|
+
try:
|
|
21
|
+
response = await call_next(request)
|
|
22
|
+
return response
|
|
23
|
+
except Exception as e:
|
|
24
|
+
self.client.capture(e, request)
|
|
25
|
+
raise
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_fastapi(app: FastAPI, client: ReliabilityClient):
|
|
29
|
+
"""Setup FastAPI integration"""
|
|
30
|
+
app.add_middleware(FastAPIMiddleware, client=client)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_fastapi_middleware(client: ReliabilityClient) -> Type[FastAPIMiddleware]:
|
|
34
|
+
"""Create a configured FastAPI middleware class for manual use with add_middleware"""
|
|
35
|
+
class _Configured(FastAPIMiddleware):
|
|
36
|
+
def __init__(self, app: FastAPI):
|
|
37
|
+
super().__init__(app, client)
|
|
38
|
+
return _Configured
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from flask import Flask, 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, app: Callable, client: ReliabilityClient):
|
|
15
|
+
self.app = app
|
|
16
|
+
self.client = client
|
|
17
|
+
|
|
18
|
+
def __call__(self, environ: dict, start_response: Callable) -> Response:
|
|
19
|
+
# Wrap start_response to capture exceptions
|
|
20
|
+
def wrapped_start_response(status, headers, exc_info=None):
|
|
21
|
+
return start_response(status, headers, exc_info)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# Use Flask's internal mechanism
|
|
25
|
+
from flask import g
|
|
26
|
+
|
|
27
|
+
return self.app(environ, wrapped_start_response)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
# Get request object from Flask
|
|
30
|
+
from flask import request
|
|
31
|
+
|
|
32
|
+
self.client.capture(e, request)
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def init_flask(app: Flask, client: ReliabilityClient):
|
|
37
|
+
"""Initialize Flask integration"""
|
|
38
|
+
|
|
39
|
+
@app.errorhandler(Exception)
|
|
40
|
+
def handle_error(error):
|
|
41
|
+
from flask import request
|
|
42
|
+
|
|
43
|
+
client.capture(error, request)
|
|
44
|
+
return {"error": "Internal server error"}, 500
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_flask_error_handler(client: ReliabilityClient):
|
|
48
|
+
"""Create Flask error handler for manual use"""
|
|
49
|
+
|
|
50
|
+
def handle_error(error):
|
|
51
|
+
from flask import request
|
|
52
|
+
|
|
53
|
+
client.capture(error, request)
|
|
54
|
+
return {"error": "Internal server error"}, 500
|
|
55
|
+
|
|
56
|
+
return handle_error
|
reliability/payload.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error Payload for Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import platform
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import traceback
|
|
10
|
+
from types import TracebackType
|
|
11
|
+
from typing import Any, Dict, Optional, TypedDict, Union
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ServiceContext(TypedDict, total=False):
|
|
16
|
+
pid: int
|
|
17
|
+
runtime: str
|
|
18
|
+
runtimeVersion: str
|
|
19
|
+
pythonVersion: str
|
|
20
|
+
platform: str
|
|
21
|
+
hostname: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RequestContext(TypedDict, total=False):
|
|
25
|
+
method: str
|
|
26
|
+
url: str
|
|
27
|
+
ip: str
|
|
28
|
+
userAgent: str
|
|
29
|
+
headers: Dict[str, Any]
|
|
30
|
+
body: Any
|
|
31
|
+
query: Any
|
|
32
|
+
cookies: Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ErrorPayload(TypedDict, total=False):
|
|
36
|
+
message: str
|
|
37
|
+
name: str
|
|
38
|
+
stack: str
|
|
39
|
+
fingerprint: str
|
|
40
|
+
timestamp: int
|
|
41
|
+
service: str
|
|
42
|
+
environment: str
|
|
43
|
+
version: str
|
|
44
|
+
release: str
|
|
45
|
+
commitHash: str
|
|
46
|
+
branch: str
|
|
47
|
+
buildId: str
|
|
48
|
+
deploymentId: str
|
|
49
|
+
occurrenceCount: int
|
|
50
|
+
severity: str
|
|
51
|
+
errorType: str
|
|
52
|
+
serviceContext: ServiceContext
|
|
53
|
+
requestContext: RequestContext
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _generate_fingerprint(error: Exception, stack: Optional[str] = None) -> str:
|
|
57
|
+
signature = str(error)
|
|
58
|
+
if stack:
|
|
59
|
+
first_line = stack.split("\n")[0] if stack else ""
|
|
60
|
+
signature += first_line
|
|
61
|
+
return hashlib.sha256(signature.encode()).hexdigest()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_SENSITIVE_KEYS = {
|
|
65
|
+
"authorization",
|
|
66
|
+
"cookie",
|
|
67
|
+
"set-cookie",
|
|
68
|
+
"x-api-key",
|
|
69
|
+
"api-key",
|
|
70
|
+
"apikey",
|
|
71
|
+
"password",
|
|
72
|
+
"token",
|
|
73
|
+
"access_token",
|
|
74
|
+
"refresh_token",
|
|
75
|
+
"secret",
|
|
76
|
+
"signature",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _sanitize_text(value: str) -> str:
|
|
81
|
+
value = re.sub(r"Bearer\s+\S+", "Bearer [REDACTED]", value)
|
|
82
|
+
for keyword in ("token", "password", "secret", "api_key", "apikey"):
|
|
83
|
+
value = re.sub(rf"{re.escape(keyword)}=\S*", f"{keyword}=[REDACTED]", value)
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _sanitize_value(value: Any) -> Any:
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
return _sanitize_text(value)
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
return {
|
|
92
|
+
key: "[REDACTED]" if key.lower() in _SENSITIVE_KEYS else _sanitize_value(item)
|
|
93
|
+
for key, item in value.items()
|
|
94
|
+
}
|
|
95
|
+
if isinstance(value, list):
|
|
96
|
+
return [_sanitize_value(item) for item in value]
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _classify_error(error: Exception) -> str:
|
|
101
|
+
msg = str(error).lower()
|
|
102
|
+
if any(
|
|
103
|
+
keyword in msg
|
|
104
|
+
for keyword in ["database", "sql", "psycopg", "mysql", "sqlite"]
|
|
105
|
+
):
|
|
106
|
+
return "DATABASE"
|
|
107
|
+
if any(
|
|
108
|
+
keyword in msg
|
|
109
|
+
for keyword in ["network", "http", "socket", "connection", "timeout", "ssl"]
|
|
110
|
+
):
|
|
111
|
+
return "NETWORK"
|
|
112
|
+
if any(
|
|
113
|
+
keyword in msg
|
|
114
|
+
for keyword in ["validation", "invalid", "required", "constraint", "type"]
|
|
115
|
+
):
|
|
116
|
+
return "VALIDATION"
|
|
117
|
+
if any(
|
|
118
|
+
keyword in msg
|
|
119
|
+
for keyword in ["permission", "denied", "forbidden", "unauthorized"]
|
|
120
|
+
):
|
|
121
|
+
return "AUTH"
|
|
122
|
+
return "RUNTIME"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _determine_severity(error: Exception) -> str:
|
|
126
|
+
msg = str(error).lower()
|
|
127
|
+
if any(
|
|
128
|
+
keyword in msg
|
|
129
|
+
for keyword in ["database", "connection", "timeout", "fatal", "crash"]
|
|
130
|
+
):
|
|
131
|
+
return "critical"
|
|
132
|
+
return "medium"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _format_stack_trace(stack: Optional[Union[str, TracebackType]] = None) -> Optional[str]:
|
|
136
|
+
if not stack:
|
|
137
|
+
return None
|
|
138
|
+
if isinstance(stack, str):
|
|
139
|
+
return stack
|
|
140
|
+
try:
|
|
141
|
+
return "".join(traceback.format_tb(stack))
|
|
142
|
+
except Exception:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _build_service_context() -> ServiceContext:
|
|
147
|
+
return {
|
|
148
|
+
"pid": os.getpid(),
|
|
149
|
+
"runtime": "python",
|
|
150
|
+
"runtimeVersion": platform.python_version(),
|
|
151
|
+
"pythonVersion": platform.python_version(),
|
|
152
|
+
"platform": platform.platform(),
|
|
153
|
+
"hostname": platform.node() or os.environ.get("HOSTNAME", "unknown"),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extract_request_context(request: Optional[Any] = None) -> Optional[RequestContext]:
|
|
158
|
+
if request is None:
|
|
159
|
+
return None
|
|
160
|
+
context: RequestContext = {}
|
|
161
|
+
|
|
162
|
+
if hasattr(request, "method"):
|
|
163
|
+
context["method"] = request.method
|
|
164
|
+
if hasattr(request, "url"):
|
|
165
|
+
context["url"] = str(request.url)
|
|
166
|
+
if hasattr(request, "client") and request.client:
|
|
167
|
+
context["ip"] = request.client.host
|
|
168
|
+
if hasattr(request, "remote_addr"):
|
|
169
|
+
context["ip"] = request.remote_addr
|
|
170
|
+
if hasattr(request, "path"):
|
|
171
|
+
context["url"] = context.get("url", request.path)
|
|
172
|
+
if hasattr(request, "META"):
|
|
173
|
+
context["ip"] = request.META.get("REMOTE_ADDR")
|
|
174
|
+
context["method"] = request.method
|
|
175
|
+
context["url"] = request.path
|
|
176
|
+
if hasattr(request, "headers"):
|
|
177
|
+
headers = dict(request.headers)
|
|
178
|
+
forwarded = headers.get("x-forwarded-for")
|
|
179
|
+
if forwarded and not context.get("ip"):
|
|
180
|
+
context["ip"] = forwarded.split(",")[0].strip()
|
|
181
|
+
user_agent = headers.get("user-agent")
|
|
182
|
+
if user_agent:
|
|
183
|
+
context["userAgent"] = user_agent
|
|
184
|
+
context["headers"] = _sanitize_value(headers)
|
|
185
|
+
|
|
186
|
+
body = request.json if hasattr(request, "json") and not callable(request.json) else None
|
|
187
|
+
if body is not None:
|
|
188
|
+
context["body"] = _sanitize_value(body)
|
|
189
|
+
elif hasattr(request, "body") and request.body is not None:
|
|
190
|
+
context["body"] = _sanitize_value(request.body)
|
|
191
|
+
|
|
192
|
+
if hasattr(request, "args") and request.args is not None:
|
|
193
|
+
context["query"] = _sanitize_value(dict(request.args))
|
|
194
|
+
if hasattr(request, "cookies") and request.cookies is not None:
|
|
195
|
+
context["cookies"] = _sanitize_value(dict(request.cookies))
|
|
196
|
+
return context if context else None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def create_error_payload(
|
|
200
|
+
error: Exception,
|
|
201
|
+
service_id: str,
|
|
202
|
+
mode: str,
|
|
203
|
+
request: Optional[Any] = None,
|
|
204
|
+
*,
|
|
205
|
+
version: Optional[str] = None,
|
|
206
|
+
release: Optional[str] = None,
|
|
207
|
+
commit_hash: Optional[str] = None,
|
|
208
|
+
branch: Optional[str] = None,
|
|
209
|
+
build_id: Optional[str] = None,
|
|
210
|
+
deployment_id: Optional[str] = None,
|
|
211
|
+
) -> ErrorPayload:
|
|
212
|
+
stack = getattr(error, "__fake_tb__", None) or _format_stack_trace(
|
|
213
|
+
error.__traceback__
|
|
214
|
+
)
|
|
215
|
+
fingerprint = _generate_fingerprint(error, stack)
|
|
216
|
+
return {
|
|
217
|
+
"message": str(error),
|
|
218
|
+
"name": type(error).__name__,
|
|
219
|
+
"stack": stack or "",
|
|
220
|
+
"fingerprint": fingerprint,
|
|
221
|
+
"timestamp": int(datetime.now().timestamp() * 1000),
|
|
222
|
+
"service": service_id,
|
|
223
|
+
"environment": mode,
|
|
224
|
+
**({"version": version} if version else {}),
|
|
225
|
+
**({"release": release} if release else {}),
|
|
226
|
+
**({"commitHash": commit_hash} if commit_hash else {}),
|
|
227
|
+
**({"branch": branch} if branch else {}),
|
|
228
|
+
**({"buildId": build_id} if build_id else {}),
|
|
229
|
+
**({"deploymentId": deployment_id} if deployment_id else {}),
|
|
230
|
+
"occurrenceCount": 1,
|
|
231
|
+
"severity": _determine_severity(error),
|
|
232
|
+
"errorType": _classify_error(error),
|
|
233
|
+
"serviceContext": _build_service_context(),
|
|
234
|
+
**(
|
|
235
|
+
{"requestContext": request_context}
|
|
236
|
+
if (request_context := _extract_request_context(request)) is not None
|
|
237
|
+
else {}
|
|
238
|
+
),
|
|
239
|
+
}
|
reliability/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xecurecode-reliability-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: AI-driven reliability SDK for Python applications
|
|
5
|
+
Author-email: XecureCode Team <team@xecurecode.in>
|
|
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
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: fastapi
|
|
20
|
+
Requires-Dist: fastapi>=0.100; extra == "fastapi"
|
|
21
|
+
Provides-Extra: flask
|
|
22
|
+
Requires-Dist: flask>=2.0; extra == "flask"
|
|
23
|
+
Provides-Extra: django
|
|
24
|
+
Requires-Dist: django>=3.0; extra == "django"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# XecureCode Reliability SDK - Python
|
|
28
|
+
|
|
29
|
+
AI-driven reliability SDK for Python applications.
|
|
30
|
+
|
|
31
|
+
The official Python SDK for the XecureCode Reliability Platform — a human-first failure analysis and recovery recommendation system.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Why This SDK Exists
|
|
36
|
+
|
|
37
|
+
Modern backend systems fail in unpredictable ways.
|
|
38
|
+
|
|
39
|
+
This SDK:
|
|
40
|
+
|
|
41
|
+
- Captures structured runtime errors
|
|
42
|
+
- Generates deterministic fingerprints
|
|
43
|
+
- Classifies error types (DATABASE, NETWORK, VALIDATION, etc.)
|
|
44
|
+
- Determines severity
|
|
45
|
+
- Sends non-blocking telemetry to your backend
|
|
46
|
+
- Never crashes your application
|
|
47
|
+
|
|
48
|
+
**AI assists. Humans decide. Nothing executes automatically.**
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Python 3.8+
|
|
55
|
+
- FastAPI, Flask, or Django (optional)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install xecurecode-reliability-sdk
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or install with framework support:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install xecurecode-reliability-sdk[fastapi,flask,django]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
### 1️⃣ Initialize the SDK
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from reliability import ReliabilityClient, ReliabilityConfig
|
|
79
|
+
|
|
80
|
+
config = ReliabilityConfig(
|
|
81
|
+
api_key="your-api-key",
|
|
82
|
+
service_id="your-service",
|
|
83
|
+
mode="development"
|
|
84
|
+
)
|
|
85
|
+
client = ReliabilityClient(config)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Configuration Options:**
|
|
89
|
+
|
|
90
|
+
| Parameter | Required | Description |
|
|
91
|
+
|-----------|----------|-------------|
|
|
92
|
+
| `api_key` | Yes | Your API key |
|
|
93
|
+
| `service_id` | Yes | Service identifier |
|
|
94
|
+
| `mode` | Yes | `development` or `production` |
|
|
95
|
+
| `timeout` | No | Request timeout (default: 5000ms) |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### 2️⃣ Automatic Global Error Capture
|
|
100
|
+
|
|
101
|
+
The SDK automatically handles:
|
|
102
|
+
- Uncaught exceptions
|
|
103
|
+
- Thread exceptions
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# This will be automatically captured
|
|
107
|
+
raise ValueError("Database timeout")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### 3️⃣ FastAPI Integration
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from fastapi import FastAPI
|
|
116
|
+
from reliability import ReliabilityClient, ReliabilityConfig
|
|
117
|
+
from reliability.fastapi_integration import FastAPIMiddleware
|
|
118
|
+
|
|
119
|
+
config = ReliabilityConfig(...)
|
|
120
|
+
client = ReliabilityClient(config)
|
|
121
|
+
|
|
122
|
+
app = FastAPI()
|
|
123
|
+
app.add_middleware(FastAPIMiddleware, client=client)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### 4️⃣ Flask Integration
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from flask import Flask
|
|
132
|
+
from reliability import ReliabilityClient, ReliabilityConfig
|
|
133
|
+
from reliability.flask_integration import init_flask
|
|
134
|
+
|
|
135
|
+
config = ReliabilityConfig(...)
|
|
136
|
+
client = ReliabilityClient(config)
|
|
137
|
+
|
|
138
|
+
app = Flask(__name__)
|
|
139
|
+
init_flask(app, client)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### 5️⃣ Django Integration
|
|
145
|
+
|
|
146
|
+
In `settings.py`:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
RELIABILITY_API_KEY = "your-api-key"
|
|
150
|
+
RELIABILITY_SERVICE_ID = "your-service"
|
|
151
|
+
RELIABILITY_MODE = "development"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
In `settings.py` MIDDLEWARE:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
MIDDLEWARE = [
|
|
158
|
+
...
|
|
159
|
+
'reliability.django_integration.ReliabilityMiddleware',
|
|
160
|
+
...
|
|
161
|
+
]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### 6️⃣ Manual Capture
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
try:
|
|
170
|
+
risky_operation()
|
|
171
|
+
except Exception as e:
|
|
172
|
+
client.capture(e)
|
|
173
|
+
|
|
174
|
+
# With request context
|
|
175
|
+
client.capture(e, request)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## What Gets Sent
|
|
181
|
+
|
|
182
|
+
Example structured payload:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"message": "Database connection failed",
|
|
187
|
+
"name": "ValueError",
|
|
188
|
+
"fingerprint": "a8c39c21...",
|
|
189
|
+
"timestamp": 1700000000000,
|
|
190
|
+
"service": "my-service",
|
|
191
|
+
"environment": "development",
|
|
192
|
+
"severity": "critical",
|
|
193
|
+
"errorType": "DATABASE",
|
|
194
|
+
"serviceContext": {
|
|
195
|
+
"pid": 12345,
|
|
196
|
+
"runtime": "python",
|
|
197
|
+
"runtimeVersion": "3.11.0",
|
|
198
|
+
"pythonVersion": "3.11.0",
|
|
199
|
+
"platform": "Linux-5.10.0",
|
|
200
|
+
"hostname": "server-01"
|
|
201
|
+
},
|
|
202
|
+
"requestContext": {
|
|
203
|
+
"method": "GET",
|
|
204
|
+
"url": "/api/users",
|
|
205
|
+
"ip": "10.0.0.5"
|
|
206
|
+
},
|
|
207
|
+
"occurrenceCount": 1
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Error Classification
|
|
214
|
+
|
|
215
|
+
| Type | Example |
|
|
216
|
+
|------|---------|
|
|
217
|
+
| DATABASE | Timeout, SQL errors, connection issues |
|
|
218
|
+
| NETWORK | HTTP failures, socket errors |
|
|
219
|
+
| VALIDATION | Invalid input, type errors |
|
|
220
|
+
| AUTH | Permission denied |
|
|
221
|
+
| RUNTIME | Standard Python errors |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Severity Detection
|
|
226
|
+
|
|
227
|
+
- **Critical** → Database, timeout, connection errors
|
|
228
|
+
- **Medium** → Validation, recoverable issues
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Design Guarantees
|
|
233
|
+
|
|
234
|
+
- Non-blocking HTTP calls
|
|
235
|
+
- Timeout protected (5s default)
|
|
236
|
+
- Retry logic (3 attempts)
|
|
237
|
+
- Deduplication (1-minute window)
|
|
238
|
+
- Rate limiting (max 100 concurrent)
|
|
239
|
+
- Never throws inside SDK
|
|
240
|
+
- Never crashes host application
|
|
241
|
+
- Works with FastAPI, Flask, Django
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
reliability/__init__.py,sha256=Cw_L9TJjEXCp6AM0Fbp8oLlfTsU5TOz2o3VJ0pufeyI,329
|
|
2
|
+
reliability/client.py,sha256=IYTvdGLcfPi6t0OlOKmCbrQBFDNCRnShTfywpORvXzE,8805
|
|
3
|
+
reliability/config.py,sha256=SBclrncIc7ntVdrTnXHEU3DJ8HZVyDjhtkjiPa8AuxQ,963
|
|
4
|
+
reliability/django_integration.py,sha256=6iw8sc0FybV1DyJwBLxN08L2eOtLZYMiXm_KusluBFE,1871
|
|
5
|
+
reliability/fastapi_integration.py,sha256=nx6znZGQK_oIu7pYWF-nRcFzoyhNFn9AQ1pgRI18KBs,1170
|
|
6
|
+
reliability/flask_integration.py,sha256=9WigBhUJJl0ATbNc_KAdEBniaHRhpGPsWBrUDKTbrPU,1490
|
|
7
|
+
reliability/payload.py,sha256=oVBkT8CtxLL1JhMU9_gKja7vt3i3y-g8aTz3bDgxAGQ,6988
|
|
8
|
+
reliability/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
xecurecode_reliability_sdk-0.1.1.dist-info/licenses/LICENSE,sha256=n61q08jxPdC1UKnkZMidrcD_iAksOya6fWZzXWg5ydE,1068
|
|
10
|
+
xecurecode_reliability_sdk-0.1.1.dist-info/METADATA,sha256=j2JLuKBezwjP-UqZEwBRllubMme0ZrbY14tLDTikh0g,5081
|
|
11
|
+
xecurecode_reliability_sdk-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
xecurecode_reliability_sdk-0.1.1.dist-info/top_level.txt,sha256=fVLL6FAOvV9ryG189ku0Ed6AhXksyefSi9HvqgmBBHo,12
|
|
13
|
+
xecurecode_reliability_sdk-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XecureTrace
|
|
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 @@
|
|
|
1
|
+
reliability
|