WaveGuardClient 2.0.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.
- waveguard/__init__.py +53 -0
- waveguard/client.py +363 -0
- waveguard/exceptions.py +63 -0
- waveguardclient-2.0.0.dist-info/METADATA +306 -0
- waveguardclient-2.0.0.dist-info/RECORD +8 -0
- waveguardclient-2.0.0.dist-info/WHEEL +5 -0
- waveguardclient-2.0.0.dist-info/licenses/LICENSE +21 -0
- waveguardclient-2.0.0.dist-info/top_level.txt +1 -0
waveguard/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WaveGuard — Physics-based anomaly detection SDK.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from waveguard import WaveGuard
|
|
7
|
+
|
|
8
|
+
wg = WaveGuard(api_key="YOUR_KEY")
|
|
9
|
+
result = wg.scan(training=normal_data, test=new_data)
|
|
10
|
+
|
|
11
|
+
for r in result.results:
|
|
12
|
+
print(r.is_anomaly, r.score, r.confidence)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .client import ( # noqa: F401
|
|
16
|
+
WaveGuard,
|
|
17
|
+
ScanResult,
|
|
18
|
+
SampleResult,
|
|
19
|
+
ScanSummary,
|
|
20
|
+
FeatureInfo,
|
|
21
|
+
EngineInfo,
|
|
22
|
+
HealthStatus,
|
|
23
|
+
TierInfo,
|
|
24
|
+
__version__,
|
|
25
|
+
)
|
|
26
|
+
from .exceptions import ( # noqa: F401
|
|
27
|
+
WaveGuardError,
|
|
28
|
+
AuthenticationError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
RateLimitError,
|
|
31
|
+
ServerError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Client
|
|
36
|
+
"WaveGuard",
|
|
37
|
+
# Results
|
|
38
|
+
"ScanResult",
|
|
39
|
+
"SampleResult",
|
|
40
|
+
"ScanSummary",
|
|
41
|
+
"FeatureInfo",
|
|
42
|
+
"EngineInfo",
|
|
43
|
+
"HealthStatus",
|
|
44
|
+
"TierInfo",
|
|
45
|
+
# Exceptions
|
|
46
|
+
"WaveGuardError",
|
|
47
|
+
"AuthenticationError",
|
|
48
|
+
"ValidationError",
|
|
49
|
+
"RateLimitError",
|
|
50
|
+
"ServerError",
|
|
51
|
+
# Meta
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
waveguard/client.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WaveGuard Python SDK — stateless anomaly detection via wave physics.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
from waveguard import WaveGuard
|
|
7
|
+
|
|
8
|
+
wg = WaveGuard(api_key="YOUR_KEY")
|
|
9
|
+
|
|
10
|
+
result = wg.scan(
|
|
11
|
+
training=[
|
|
12
|
+
{"cpu": 45, "memory": 62, "errors": 0},
|
|
13
|
+
{"cpu": 48, "memory": 63, "errors": 0},
|
|
14
|
+
{"cpu": 42, "memory": 61, "errors": 1},
|
|
15
|
+
{"cpu": 50, "memory": 64, "errors": 0},
|
|
16
|
+
],
|
|
17
|
+
test=[
|
|
18
|
+
{"cpu": 46, "memory": 62, "errors": 0}, # normal
|
|
19
|
+
{"cpu": 99, "memory": 95, "errors": 150}, # anomaly
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
for r in result.results:
|
|
24
|
+
print(r.is_anomaly, r.score, r.confidence)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import requests
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
from .exceptions import (
|
|
34
|
+
WaveGuardError,
|
|
35
|
+
AuthenticationError,
|
|
36
|
+
ValidationError,
|
|
37
|
+
RateLimitError,
|
|
38
|
+
ServerError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "2.0.0"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ─────────────────────────────── Data Classes ─────────────────────────────
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class FeatureInfo:
|
|
49
|
+
"""A single top-contributing feature in anomaly scoring."""
|
|
50
|
+
|
|
51
|
+
dimension: int
|
|
52
|
+
label: str
|
|
53
|
+
z_score: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class EngineInfo:
|
|
58
|
+
"""Physics engine configuration for this scan."""
|
|
59
|
+
|
|
60
|
+
grid_size: int
|
|
61
|
+
evolution_steps: int
|
|
62
|
+
fingerprint_dims: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class SampleResult:
|
|
67
|
+
"""Result of anomaly detection on a single test sample."""
|
|
68
|
+
|
|
69
|
+
score: float
|
|
70
|
+
is_anomaly: bool
|
|
71
|
+
threshold: float
|
|
72
|
+
mahalanobis_distance: float
|
|
73
|
+
confidence: float
|
|
74
|
+
top_features: List[FeatureInfo]
|
|
75
|
+
latency_ms: float
|
|
76
|
+
engine: EngineInfo
|
|
77
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ScanSummary:
|
|
82
|
+
"""Aggregate statistics from a scan."""
|
|
83
|
+
|
|
84
|
+
total_test_samples: int
|
|
85
|
+
total_training_samples: int
|
|
86
|
+
anomalies_found: int
|
|
87
|
+
anomaly_rate: float
|
|
88
|
+
mean_score: float
|
|
89
|
+
max_score: float
|
|
90
|
+
total_latency_ms: float
|
|
91
|
+
encoder_type: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ScanResult:
|
|
96
|
+
"""Complete result of a ``/v1/scan`` call.
|
|
97
|
+
|
|
98
|
+
Attributes
|
|
99
|
+
----------
|
|
100
|
+
results : list[SampleResult]
|
|
101
|
+
One entry per test sample, in order.
|
|
102
|
+
summary : ScanSummary
|
|
103
|
+
Aggregate statistics across all test samples.
|
|
104
|
+
raw : dict
|
|
105
|
+
The full JSON response from the API.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
results: List[SampleResult]
|
|
109
|
+
summary: ScanSummary
|
|
110
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class HealthStatus:
|
|
115
|
+
"""API health information."""
|
|
116
|
+
|
|
117
|
+
status: str
|
|
118
|
+
version: str
|
|
119
|
+
gpu: str
|
|
120
|
+
mode: str
|
|
121
|
+
uptime_seconds: float
|
|
122
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class TierInfo:
|
|
127
|
+
"""Current subscription tier and rate limits."""
|
|
128
|
+
|
|
129
|
+
tier: str
|
|
130
|
+
limits: Dict[str, int]
|
|
131
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ─────────────────────────────── Client ───────────────────────────────────
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class WaveGuard:
|
|
138
|
+
"""WaveGuard Anomaly Detection client.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
api_key : str
|
|
143
|
+
Your WaveGuard API key.
|
|
144
|
+
base_url : str, optional
|
|
145
|
+
API base URL. Defaults to the production Modal endpoint.
|
|
146
|
+
timeout : float, optional
|
|
147
|
+
Request timeout in seconds. Default ``120`` (generous for GPU
|
|
148
|
+
cold starts).
|
|
149
|
+
|
|
150
|
+
Examples
|
|
151
|
+
--------
|
|
152
|
+
>>> wg = WaveGuard(api_key="wg_test_key")
|
|
153
|
+
>>> result = wg.scan(
|
|
154
|
+
... training=[{"a": 1}, {"a": 2}, {"a": 3}],
|
|
155
|
+
... test=[{"a": 100}],
|
|
156
|
+
... )
|
|
157
|
+
>>> result.results[0].is_anomaly
|
|
158
|
+
True
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
DEFAULT_URL = "https://gpartin--waveguard-api-fastapi-app.modal.run"
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
api_key: str,
|
|
166
|
+
base_url: str = DEFAULT_URL,
|
|
167
|
+
timeout: float = 120.0,
|
|
168
|
+
):
|
|
169
|
+
self.api_key = api_key
|
|
170
|
+
self.base_url = base_url.rstrip("/")
|
|
171
|
+
self.timeout = timeout
|
|
172
|
+
self._session = requests.Session()
|
|
173
|
+
self._session.headers.update(
|
|
174
|
+
{
|
|
175
|
+
"X-API-Key": api_key,
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
"User-Agent": f"waveguard-python/{__version__}",
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# ── Core API ──────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
def scan(
|
|
184
|
+
self,
|
|
185
|
+
training: List[Any],
|
|
186
|
+
test: List[Any],
|
|
187
|
+
encoder_type: Optional[str] = None,
|
|
188
|
+
sensitivity: Optional[float] = None,
|
|
189
|
+
) -> ScanResult:
|
|
190
|
+
"""Scan test data for anomalies against a training baseline.
|
|
191
|
+
|
|
192
|
+
This is the only method you need. Send training + test data in a
|
|
193
|
+
single call, get anomaly scores back, and everything is torn down.
|
|
194
|
+
Fully stateless — nothing persists between calls.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
training : list
|
|
199
|
+
2+ examples of **normal** data. All entries must be the same
|
|
200
|
+
type and shape.
|
|
201
|
+
test : list
|
|
202
|
+
1+ samples to check for anomalies.
|
|
203
|
+
encoder_type : str, optional
|
|
204
|
+
Force a specific encoder: ``"json"``, ``"numeric"``,
|
|
205
|
+
``"text"``, ``"timeseries"``, ``"tabular"``.
|
|
206
|
+
Leave *None* for auto-detection (recommended).
|
|
207
|
+
sensitivity : float, optional
|
|
208
|
+
Detection sensitivity in the range 0.5–3.0.
|
|
209
|
+
Lower values are more sensitive (flag more anomalies).
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
ScanResult
|
|
214
|
+
``.results`` has one :class:`SampleResult` per test sample.
|
|
215
|
+
``.summary`` has aggregate stats.
|
|
216
|
+
"""
|
|
217
|
+
body: Dict[str, Any] = {
|
|
218
|
+
"training": training,
|
|
219
|
+
"test": test,
|
|
220
|
+
}
|
|
221
|
+
if encoder_type is not None:
|
|
222
|
+
body["encoder_type"] = encoder_type
|
|
223
|
+
if sensitivity is not None:
|
|
224
|
+
body["sensitivity"] = sensitivity
|
|
225
|
+
|
|
226
|
+
resp = self._post("/v1/scan", body)
|
|
227
|
+
return self._parse_scan(resp, len(training), len(test))
|
|
228
|
+
|
|
229
|
+
# ── Utility ───────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
def health(self) -> HealthStatus:
|
|
232
|
+
"""Check API health and GPU status. No auth required."""
|
|
233
|
+
resp = self._get("/v1/health")
|
|
234
|
+
return HealthStatus(
|
|
235
|
+
status=resp.get("status", "unknown"),
|
|
236
|
+
version=resp.get("version", ""),
|
|
237
|
+
gpu=resp.get("gpu", ""),
|
|
238
|
+
mode=resp.get("mode", ""),
|
|
239
|
+
uptime_seconds=resp.get("uptime_seconds", 0.0),
|
|
240
|
+
raw=resp,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def tier(self) -> TierInfo:
|
|
244
|
+
"""Get current subscription tier and rate limits."""
|
|
245
|
+
resp = self._get("/v1/tier")
|
|
246
|
+
return TierInfo(
|
|
247
|
+
tier=resp.get("tier", ""),
|
|
248
|
+
limits=resp.get("limits", {}),
|
|
249
|
+
raw=resp,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# ── Response parsing ──────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
def _parse_scan(
|
|
255
|
+
self, resp: dict, n_train: int, n_test: int
|
|
256
|
+
) -> ScanResult:
|
|
257
|
+
results = []
|
|
258
|
+
for r in resp.get("results", []):
|
|
259
|
+
features = [
|
|
260
|
+
FeatureInfo(
|
|
261
|
+
dimension=f.get("dimension", 0),
|
|
262
|
+
label=f.get("label", ""),
|
|
263
|
+
z_score=f.get("z_score", 0.0),
|
|
264
|
+
)
|
|
265
|
+
for f in r.get("top_features", [])
|
|
266
|
+
]
|
|
267
|
+
eng = r.get("engine", {})
|
|
268
|
+
results.append(
|
|
269
|
+
SampleResult(
|
|
270
|
+
score=r.get("score", 0.0),
|
|
271
|
+
is_anomaly=r.get("is_anomaly", False),
|
|
272
|
+
threshold=r.get("threshold", 0.0),
|
|
273
|
+
mahalanobis_distance=r.get("mahalanobis_distance", 0.0),
|
|
274
|
+
confidence=r.get("confidence", 0.0),
|
|
275
|
+
top_features=features,
|
|
276
|
+
latency_ms=r.get("latency_ms", 0.0),
|
|
277
|
+
engine=EngineInfo(
|
|
278
|
+
grid_size=eng.get("grid_size", 32),
|
|
279
|
+
evolution_steps=eng.get("evolution_steps", 150),
|
|
280
|
+
fingerprint_dims=eng.get("fingerprint_dims", 52),
|
|
281
|
+
),
|
|
282
|
+
raw=r,
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
s = resp.get("summary", {})
|
|
287
|
+
summary = ScanSummary(
|
|
288
|
+
total_test_samples=s.get("total_test_samples", n_test),
|
|
289
|
+
total_training_samples=s.get("total_training_samples", n_train),
|
|
290
|
+
anomalies_found=s.get("anomalies_found", 0),
|
|
291
|
+
anomaly_rate=s.get("anomaly_rate", 0.0),
|
|
292
|
+
mean_score=s.get("mean_score", 0.0),
|
|
293
|
+
max_score=s.get("max_score", 0.0),
|
|
294
|
+
total_latency_ms=s.get("total_latency_ms", 0.0),
|
|
295
|
+
encoder_type=s.get("encoder_type", "auto"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return ScanResult(results=results, summary=summary, raw=resp)
|
|
299
|
+
|
|
300
|
+
# ── Internal HTTP ─────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
303
|
+
url = f"{self.base_url}{path}"
|
|
304
|
+
try:
|
|
305
|
+
r = self._session.post(url, json=body, timeout=self.timeout)
|
|
306
|
+
except requests.ConnectionError:
|
|
307
|
+
raise WaveGuardError(f"Cannot connect to {self.base_url}")
|
|
308
|
+
except requests.Timeout:
|
|
309
|
+
raise WaveGuardError(
|
|
310
|
+
f"Request timed out after {self.timeout}s"
|
|
311
|
+
)
|
|
312
|
+
return self._handle(r)
|
|
313
|
+
|
|
314
|
+
def _get(self, path: str) -> dict:
|
|
315
|
+
url = f"{self.base_url}{path}"
|
|
316
|
+
try:
|
|
317
|
+
r = self._session.get(url, timeout=self.timeout)
|
|
318
|
+
except requests.ConnectionError:
|
|
319
|
+
raise WaveGuardError(f"Cannot connect to {self.base_url}")
|
|
320
|
+
except requests.Timeout:
|
|
321
|
+
raise WaveGuardError(
|
|
322
|
+
f"Request timed out after {self.timeout}s"
|
|
323
|
+
)
|
|
324
|
+
return self._handle(r)
|
|
325
|
+
|
|
326
|
+
def _handle(self, r: requests.Response) -> dict:
|
|
327
|
+
if r.status_code == 401:
|
|
328
|
+
raise AuthenticationError(
|
|
329
|
+
"Invalid or missing API key",
|
|
330
|
+
status_code=401,
|
|
331
|
+
detail=r.text,
|
|
332
|
+
)
|
|
333
|
+
if r.status_code == 422:
|
|
334
|
+
raise ValidationError(
|
|
335
|
+
f"Validation failed: {r.text}",
|
|
336
|
+
status_code=422,
|
|
337
|
+
detail=r.text,
|
|
338
|
+
)
|
|
339
|
+
if r.status_code == 429:
|
|
340
|
+
raise RateLimitError(
|
|
341
|
+
f"Rate or tier limit exceeded: {r.text}",
|
|
342
|
+
status_code=429,
|
|
343
|
+
detail=r.text,
|
|
344
|
+
)
|
|
345
|
+
if r.status_code >= 500:
|
|
346
|
+
raise ServerError(
|
|
347
|
+
f"Server error {r.status_code}: {r.text}",
|
|
348
|
+
status_code=r.status_code,
|
|
349
|
+
detail=r.text,
|
|
350
|
+
)
|
|
351
|
+
if r.status_code >= 400:
|
|
352
|
+
raise WaveGuardError(
|
|
353
|
+
f"API error {r.status_code}: {r.text}",
|
|
354
|
+
status_code=r.status_code,
|
|
355
|
+
detail=r.text,
|
|
356
|
+
)
|
|
357
|
+
try:
|
|
358
|
+
return r.json()
|
|
359
|
+
except ValueError:
|
|
360
|
+
return {"raw": r.text}
|
|
361
|
+
|
|
362
|
+
def __repr__(self) -> str:
|
|
363
|
+
return f"WaveGuard(base_url='{self.base_url}')"
|
waveguard/exceptions.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WaveGuard exception hierarchy.
|
|
3
|
+
|
|
4
|
+
All exceptions inherit from WaveGuardError so you can catch them with a
|
|
5
|
+
single ``except WaveGuardError`` block, or handle specific cases granularly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"WaveGuardError",
|
|
10
|
+
"AuthenticationError",
|
|
11
|
+
"ValidationError",
|
|
12
|
+
"RateLimitError",
|
|
13
|
+
"ServerError",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WaveGuardError(Exception):
|
|
18
|
+
"""Base exception for all WaveGuard SDK errors.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
message : str
|
|
23
|
+
Human-readable error description.
|
|
24
|
+
status_code : int
|
|
25
|
+
HTTP status code (0 if not from an HTTP response).
|
|
26
|
+
detail : str
|
|
27
|
+
Raw error body from the API, if available.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, status_code: int = 0, detail: str = ""):
|
|
31
|
+
self.message = message
|
|
32
|
+
self.status_code = status_code
|
|
33
|
+
self.detail = detail
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AuthenticationError(WaveGuardError):
|
|
38
|
+
"""API key is invalid, expired, or missing (HTTP 401)."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ValidationError(WaveGuardError):
|
|
43
|
+
"""Request data failed server-side validation (HTTP 422).
|
|
44
|
+
|
|
45
|
+
Check ``detail`` for specifics (e.g. empty training array, type mismatch).
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RateLimitError(WaveGuardError):
|
|
51
|
+
"""Rate limit or subscription tier limit exceeded (HTTP 429).
|
|
52
|
+
|
|
53
|
+
Back off and retry, or upgrade your tier.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ServerError(WaveGuardError):
|
|
59
|
+
"""Internal server error (HTTP 5xx).
|
|
60
|
+
|
|
61
|
+
Usually transient — retry after a short delay.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: WaveGuardClient
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Python SDK for WaveGuard — physics-based anomaly detection API
|
|
5
|
+
Author: Greg Partin
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gpartin/WaveGuardClient
|
|
8
|
+
Project-URL: Documentation, https://github.com/gpartin/WaveGuardClient/tree/main/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/gpartin/WaveGuardClient
|
|
10
|
+
Project-URL: Issues, https://github.com/gpartin/WaveGuardClient/issues
|
|
11
|
+
Project-URL: API, https://gpartin--waveguard-api-fastapi-app.modal.run/docs
|
|
12
|
+
Keywords: anomaly-detection,api-client,sdk,waveguard,physics-based,mcp,model-context-protocol,azure-migration
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: System :: Monitoring
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests>=2.28
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: requests>=2.28; extra == "mcp"
|
|
30
|
+
Requires-Dist: fastapi>=0.100; extra == "mcp"
|
|
31
|
+
Requires-Dist: uvicorn>=0.20; extra == "mcp"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://img.shields.io/pypi/v/WaveGuardClient?style=for-the-badge&color=blueviolet" alt="PyPI">
|
|
39
|
+
<img src="https://img.shields.io/badge/API-v2.0.0_stateless-brightgreen?style=for-the-badge" alt="v2.0.0">
|
|
40
|
+
<img src="https://img.shields.io/badge/GPU-CUDA_accelerated-76B900?style=for-the-badge&logo=nvidia" alt="CUDA">
|
|
41
|
+
<img src="https://img.shields.io/badge/MCP-Claude_Desktop-orange?style=for-the-badge" alt="MCP">
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<h1 align="center">WaveGuard Python SDK</h1>
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<strong>Anomaly detection powered by wave physics. Not machine learning.</strong><br>
|
|
48
|
+
One API call. Fully stateless. Works on any data type.
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
<a href="#quickstart">Quickstart</a> •
|
|
53
|
+
<a href="#use-cases">Use Cases</a> •
|
|
54
|
+
<a href="#examples">Examples</a> •
|
|
55
|
+
<a href="docs/api-reference.md">API Reference</a> •
|
|
56
|
+
<a href="docs/mcp-integration.md">MCP / Claude</a> •
|
|
57
|
+
<a href="docs/azure-migration.md">Azure Migration</a>
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## What is WaveGuard?
|
|
63
|
+
|
|
64
|
+
WaveGuard is a **general-purpose anomaly detection API**. Send it any data — server metrics, financial transactions, log files, sensor readings, time series — and get back anomaly scores, confidence levels, and explanations of *which features* triggered the alert.
|
|
65
|
+
|
|
66
|
+
**No training pipelines. No model management. No state. One API call.**
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Your data → WaveGuard API (GPU) → Anomaly scores + explanations
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Under the hood, it uses GPU-accelerated wave physics instead of machine learning. You don't need to know or care about the physics — it's all server-side.
|
|
73
|
+
|
|
74
|
+
<details>
|
|
75
|
+
<summary><strong>How does it actually work?</strong></summary>
|
|
76
|
+
|
|
77
|
+
Your data is encoded onto a 32³ lattice and run through coupled wave equation simulations on GPU. Normal data produces stable wave patterns; anomalies produce divergent ones. A 52-dimensional statistical fingerprint is compared between training and test data. Everything is torn down after each call — nothing is stored.
|
|
78
|
+
|
|
79
|
+
The key advantage over ML: no training data requirements (2+ samples is enough), no model drift, no retraining, no hyperparameter tuning. Same API call works on structured data, text, numbers, and time series.
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install WaveGuardClient
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That's it. The only dependency is `requests`. All physics runs server-side on GPU.
|
|
90
|
+
|
|
91
|
+
## Quickstart
|
|
92
|
+
|
|
93
|
+
The same `scan()` call works on any data type. Here are three different industries — same API:
|
|
94
|
+
|
|
95
|
+
### Detect a compromised server
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from waveguard import WaveGuard
|
|
99
|
+
|
|
100
|
+
wg = WaveGuard(api_key="YOUR_KEY")
|
|
101
|
+
|
|
102
|
+
result = wg.scan(
|
|
103
|
+
training=[
|
|
104
|
+
{"cpu": 45, "memory": 62, "disk_io": 120, "errors": 0},
|
|
105
|
+
{"cpu": 48, "memory": 63, "disk_io": 115, "errors": 0},
|
|
106
|
+
{"cpu": 42, "memory": 61, "disk_io": 125, "errors": 1},
|
|
107
|
+
],
|
|
108
|
+
test=[
|
|
109
|
+
{"cpu": 46, "memory": 62, "disk_io": 119, "errors": 0}, # ✅ normal
|
|
110
|
+
{"cpu": 99, "memory": 95, "disk_io": 800, "errors": 150}, # 🚨 anomaly
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
for r in result.results:
|
|
115
|
+
print(f"{'🚨' if r.is_anomaly else '✅'} score={r.score:.1f} confidence={r.confidence:.0%}")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Flag a fraudulent transaction
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
result = wg.scan(
|
|
122
|
+
training=[
|
|
123
|
+
{"amount": 74.50, "items": 3, "session_sec": 340, "returning": 1},
|
|
124
|
+
{"amount": 52.00, "items": 2, "session_sec": 280, "returning": 1},
|
|
125
|
+
{"amount": 89.99, "items": 4, "session_sec": 410, "returning": 0},
|
|
126
|
+
],
|
|
127
|
+
test=[
|
|
128
|
+
{"amount": 68.00, "items": 2, "session_sec": 300, "returning": 1}, # ✅ normal
|
|
129
|
+
{"amount": 4200.00, "items": 25, "session_sec": 8, "returning": 0}, # 🚨 fraud
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Catch a security event in logs
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
result = wg.scan(
|
|
138
|
+
training=[
|
|
139
|
+
"2026-02-24 10:15:03 INFO Request processed in 45ms [200 OK]",
|
|
140
|
+
"2026-02-24 10:15:04 INFO Request processed in 52ms [200 OK]",
|
|
141
|
+
"2026-02-24 10:15:05 INFO Cache hit ratio=0.94 ttl=300s",
|
|
142
|
+
],
|
|
143
|
+
test=[
|
|
144
|
+
"2026-02-24 10:20:03 INFO Request processed in 48ms [200 OK]", # ✅ normal
|
|
145
|
+
"2026-02-24 10:20:04 CRIT xmrig consuming 98% CPU, port 45678 open", # 🚨 crypto miner
|
|
146
|
+
"2026-02-24 10:20:05 WARN GET /api/users?id=1;DROP TABLE users-- from 185.x.x", # 🚨 SQL injection
|
|
147
|
+
],
|
|
148
|
+
encoder_type="text",
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Same client. Same `scan()` call. Any data.**
|
|
153
|
+
|
|
154
|
+
## Use Cases
|
|
155
|
+
|
|
156
|
+
WaveGuard works on **any structured, numeric, or text data**. If you can describe "normal," it can detect deviations.
|
|
157
|
+
|
|
158
|
+
| Industry | What You Scan | What It Catches |
|
|
159
|
+
|----------|---------------|------------------|
|
|
160
|
+
| **DevOps** | Server metrics (CPU, memory, latency) | Memory leaks, DDoS attacks, runaway processes |
|
|
161
|
+
| **Fintech** | Transactions (amount, velocity, location) | Fraud, money laundering, account takeover |
|
|
162
|
+
| **Security** | Log files, access events | SQL injection, crypto miners, privilege escalation |
|
|
163
|
+
| **IoT / Manufacturing** | Sensor readings (temp, pressure, vibration) | Equipment failure, calibration drift |
|
|
164
|
+
| **E-commerce** | User behavior (session time, cart, clicks) | Bot traffic, bulk purchase fraud, scraping |
|
|
165
|
+
| **Healthcare** | Lab results, vitals, biomarkers | Abnormal readings, data entry errors |
|
|
166
|
+
| **Time Series** | Metric windows (latency, throughput) | Spikes, flatlines, seasonal breaks |
|
|
167
|
+
|
|
168
|
+
**The API doesn't know your domain.** It just knows what "normal" looks like (your training data) and flags anything that deviates. This makes it general — you bring the context, it brings the detection.
|
|
169
|
+
|
|
170
|
+
### Supported Data Types
|
|
171
|
+
|
|
172
|
+
All auto-detected from data shape. No configuration needed:
|
|
173
|
+
|
|
174
|
+
| Type | Example | Use When |
|
|
175
|
+
|------|---------|----------|
|
|
176
|
+
| JSON objects | `{"cpu": 45, "memory": 62}` | Structured records with named fields |
|
|
177
|
+
| Numeric arrays | `[1.0, 1.2, 5.8, 1.1]` | Feature vectors, embeddings |
|
|
178
|
+
| Text strings | `"ERROR segfault at 0x0"` | Logs, messages, free text |
|
|
179
|
+
| Time series | `[100, 102, 98, 105, 99]` | Metric windows, sequential readings |
|
|
180
|
+
|
|
181
|
+
## Examples
|
|
182
|
+
|
|
183
|
+
Every example is a runnable Python script. They span **6 industries and 4 data types** to show WaveGuard isn't tied to one domain:
|
|
184
|
+
|
|
185
|
+
| # | Example | Industry | Data Type | What It Shows |
|
|
186
|
+
|---|---------|----------|-----------|---------------|
|
|
187
|
+
| 01 | [Quickstart](examples/01_quickstart.py) | General | JSON | Minimal scan in 10 lines |
|
|
188
|
+
| 02 | [Server Monitoring](examples/02_server_monitoring.py) | DevOps | JSON | Memory leak + DDoS detection |
|
|
189
|
+
| 03 | [Log Analysis](examples/03_log_analysis.py) | Security | Text | SQL injection, crypto miner, stack traces |
|
|
190
|
+
| 04 | [Time Series](examples/04_time_series.py) | Monitoring | Numeric | Latency spikes, flatline detection |
|
|
191
|
+
| 05 | [Azure Migration](examples/05_azure_migration.py) | Enterprise | JSON | Side-by-side Azure replacement |
|
|
192
|
+
| 06 | [Batch Scanning](examples/06_batch_scanning.py) | E-commerce | JSON | 20 transactions, fraud flagging |
|
|
193
|
+
| 07 | [Error Handling](examples/07_error_handling.py) | Production | — | Retry logic, exponential backoff |
|
|
194
|
+
|
|
195
|
+
## MCP Server (Claude Desktop)
|
|
196
|
+
|
|
197
|
+
Give Claude the ability to detect anomalies. Add to your Claude Desktop config:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"mcpServers": {
|
|
202
|
+
"waveguard": {
|
|
203
|
+
"command": "python",
|
|
204
|
+
"args": ["/path/to/WaveGuardClient/mcp_server/server.py"],
|
|
205
|
+
"env": {
|
|
206
|
+
"WAVEGUARD_API_KEY": "your-key"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then ask Claude: *"Check if these server metrics are anomalous..."*
|
|
214
|
+
|
|
215
|
+
See [MCP Integration Guide](docs/mcp-integration.md) for full setup.
|
|
216
|
+
|
|
217
|
+
## Azure Migration
|
|
218
|
+
|
|
219
|
+
**Azure Anomaly Detector retires October 2026.** WaveGuard is a drop-in replacement:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
# Before (Azure) — 3+ API calls, stateful, time-series only
|
|
223
|
+
client = AnomalyDetectorClient(endpoint, credential)
|
|
224
|
+
model = client.train_multivariate_model(request) # minutes
|
|
225
|
+
result = client.detect_multivariate_batch_anomaly(model_id, data)
|
|
226
|
+
client.delete_multivariate_model(model_id)
|
|
227
|
+
|
|
228
|
+
# After (WaveGuard) — 1 API call, stateless, any data type
|
|
229
|
+
wg = WaveGuard(api_key="YOUR_KEY")
|
|
230
|
+
result = wg.scan(training=normal_data, test=new_data) # seconds
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
See [Azure Migration Guide](docs/azure-migration.md) for details.
|
|
234
|
+
|
|
235
|
+
## API Reference
|
|
236
|
+
|
|
237
|
+
### `wg.scan(training, test, encoder_type=None, sensitivity=None)`
|
|
238
|
+
|
|
239
|
+
| Parameter | Type | Description |
|
|
240
|
+
|-----------|------|-------------|
|
|
241
|
+
| `training` | `list` | 2+ examples of normal data |
|
|
242
|
+
| `test` | `list` | 1+ samples to check |
|
|
243
|
+
| `encoder_type` | `str` | Force: `"json"`, `"numeric"`, `"text"`, `"timeseries"` (default: auto) |
|
|
244
|
+
| `sensitivity` | `float` | 0.5–3.0, lower = more sensitive (default: 1.0) |
|
|
245
|
+
|
|
246
|
+
Returns `ScanResult` with `.results` (per-sample) and `.summary` (aggregate).
|
|
247
|
+
|
|
248
|
+
### `wg.health()` / `wg.tier()`
|
|
249
|
+
|
|
250
|
+
Health check (no auth) and subscription tier info.
|
|
251
|
+
|
|
252
|
+
### Error Handling
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from waveguard import WaveGuard, AuthenticationError, RateLimitError
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
result = wg.scan(training=data, test=new_data)
|
|
259
|
+
except AuthenticationError:
|
|
260
|
+
print("Bad API key")
|
|
261
|
+
except RateLimitError:
|
|
262
|
+
print("Too many requests — back off and retry")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Full API reference: [docs/api-reference.md](docs/api-reference.md)
|
|
266
|
+
|
|
267
|
+
## Project Structure
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
WaveGuardClient/
|
|
271
|
+
├── waveguard/ # Python SDK package
|
|
272
|
+
│ ├── __init__.py # Public API exports
|
|
273
|
+
│ ├── client.py # WaveGuard client class
|
|
274
|
+
│ └── exceptions.py # Exception hierarchy
|
|
275
|
+
├── mcp_server/ # MCP server for Claude Desktop
|
|
276
|
+
│ ├── server.py # stdio + HTTP transport
|
|
277
|
+
│ └── README.md # MCP setup guide
|
|
278
|
+
├── examples/ # 7 runnable examples
|
|
279
|
+
├── docs/ # Documentation
|
|
280
|
+
│ ├── getting-started.md
|
|
281
|
+
│ ├── api-reference.md
|
|
282
|
+
│ ├── mcp-integration.md
|
|
283
|
+
│ └── azure-migration.md
|
|
284
|
+
├── tests/ # Test suite (runs offline)
|
|
285
|
+
├── pyproject.toml # Package config (pip install -e .)
|
|
286
|
+
└── CHANGELOG.md
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Development
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
git clone https://github.com/gpartin/WaveGuardClient.git
|
|
293
|
+
cd WaveGuardClient
|
|
294
|
+
pip install -e ".[dev]"
|
|
295
|
+
pytest
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Links
|
|
299
|
+
|
|
300
|
+
- **Live API**: https://gpartin--waveguard-api-fastapi-app.modal.run
|
|
301
|
+
- **Interactive Docs (Swagger)**: https://gpartin--waveguard-api-fastapi-app.modal.run/docs
|
|
302
|
+
- **PyPI**: https://pypi.org/project/WaveGuardClient/
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
waveguard/__init__.py,sha256=FONe580D6xKXbfreD9l6tRvIgU2LGucadVsIlowZv_k,971
|
|
2
|
+
waveguard/client.py,sha256=OCuUwec3Z0HGat2PYob7OEGz4Vc--GqdJNmaFOF1QQU,11440
|
|
3
|
+
waveguard/exceptions.py,sha256=WX9vtgk_YptWU0d_JHGofsixL_UEBLDAasdS5YNeQic,1499
|
|
4
|
+
waveguardclient-2.0.0.dist-info/licenses/LICENSE,sha256=yV4vV4gmeJzzKCuJZPkv6esNWoR78ZIUHBIyq5vrSHA,1073
|
|
5
|
+
waveguardclient-2.0.0.dist-info/METADATA,sha256=zie4uYNXKAgdQFhAOFqAKBO-xvwNjpwe3xo3-zUf4dc,11943
|
|
6
|
+
waveguardclient-2.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
7
|
+
waveguardclient-2.0.0.dist-info/top_level.txt,sha256=JxjhFv0TRFvmE4KlZrJMq9F65YkIx9TodmCSOO5Mzvo,10
|
|
8
|
+
waveguardclient-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Greg Partin
|
|
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
|
+
waveguard
|