hooksniff-python 0.4.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.
hooksniff/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ HookSniff Python SDK - Webhook infrastructure for developers.
3
+
4
+ Usage:
5
+ from hooksniff import HookSniff
6
+
7
+ hs = HookSniff("hr_live_...")
8
+
9
+ # Create application
10
+ app = hs.application.create(name="My App")
11
+
12
+ # Create endpoint
13
+ ep = hs.endpoint.create(url="https://app.com/webhook", application_id=app["id"])
14
+
15
+ # Send webhook
16
+ delivery = hs.webhook.send(endpoint_id=ep["id"], event="order.created", data={"id": "123"})
17
+ """
18
+
19
+ from .client import HookSniff
20
+ from .webhook import Webhook, WebhookVerificationError
21
+ from .exceptions import (
22
+ HookSniffError,
23
+ AuthenticationError,
24
+ NotFoundError,
25
+ RateLimitError,
26
+ ValidationError,
27
+ ServerError,
28
+ )
29
+
30
+ __version__ = "0.4.1"
31
+ __all__ = [
32
+ "HookSniff",
33
+ "Webhook",
34
+ "WebhookVerificationError",
35
+ "HookSniffError",
36
+ "AuthenticationError",
37
+ "NotFoundError",
38
+ "RateLimitError",
39
+ "ValidationError",
40
+ "ServerError",
41
+ ]
hooksniff/client.py ADDED
@@ -0,0 +1,108 @@
1
+ """
2
+ HookSniff Python SDK client.
3
+
4
+ Usage:
5
+ from hooksniff import HookSniff
6
+
7
+ hs = HookSniff("hr_live_...")
8
+
9
+ # Create application
10
+ app = hs.application.create(name="My App")
11
+
12
+ # Create endpoint
13
+ ep = hs.endpoint.create(url="https://app.com/webhook", application_id=app["id"])
14
+
15
+ # Send webhook
16
+ delivery = hs.webhook.send(endpoint_id=ep["id"], event="order.created", data={"id": "123"})
17
+
18
+ # Auto-pagination
19
+ for ep in hs.endpoint.list():
20
+ print(ep["url"])
21
+ """
22
+
23
+ from typing import Any, Dict
24
+ from .http_client import HttpClient
25
+ from .resources import (
26
+ ApplicationResource,
27
+ EndpointResource,
28
+ WebhookResource,
29
+ ApiKeyResource,
30
+ AnalyticsResource,
31
+ SearchResource,
32
+ HealthResource,
33
+ BillingResource,
34
+ NotificationResource,
35
+ CortexResource,
36
+ TeamResource,
37
+ AlertResource,
38
+ TemplateResource,
39
+ SchemaResource,
40
+ ConnectorResource,
41
+ StreamResource,
42
+ BackgroundTaskResource,
43
+ IntegrationResource,
44
+ ServiceTokenResource,
45
+ OperationalWebhookResource,
46
+ RateLimitResource,
47
+ AuditResource,
48
+ SsoResource,
49
+ CustomDomainResource,
50
+ EnvironmentResource,
51
+ )
52
+
53
+
54
+ class HookSniff:
55
+ """
56
+ HookSniff SDK client.
57
+
58
+ Args:
59
+ api_key: Your HookSniff API key (hr_live_* or hr_test_*)
60
+ base_url: Custom API URL (optional)
61
+ timeout: Request timeout in seconds (default: 30)
62
+ retries: Max retries on 5xx/429 (default: 3)
63
+
64
+ Example:
65
+ hs = HookSniff("hr_live_...")
66
+ app = hs.application.create(name="My App")
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ api_key: str,
72
+ base_url: str = None,
73
+ timeout: int = None,
74
+ retries: int = None,
75
+ headers: Dict[str, str] = None,
76
+ ):
77
+ self.http = HttpClient(api_key, base_url, timeout, retries, headers)
78
+
79
+ # Resources
80
+ self.application = ApplicationResource(self.http)
81
+ self.endpoint = EndpointResource(self.http)
82
+ self.webhook = WebhookResource(self.http)
83
+ self.api_key = ApiKeyResource(self.http)
84
+ self.analytics = AnalyticsResource(self.http)
85
+ self.search = SearchResource(self.http)
86
+ self.health = HealthResource(self.http)
87
+ self.billing = BillingResource(self.http)
88
+ self.notification = NotificationResource(self.http)
89
+ self.cortex = CortexResource(self.http)
90
+ self.team = TeamResource(self.http)
91
+ self.alert = AlertResource(self.http)
92
+ self.template = TemplateResource(self.http)
93
+ self.schema = SchemaResource(self.http)
94
+ self.connector = ConnectorResource(self.http)
95
+ self.stream = StreamResource(self.http)
96
+ self.background_task = BackgroundTaskResource(self.http)
97
+ self.integration = IntegrationResource(self.http)
98
+ self.service_token = ServiceTokenResource(self.http)
99
+ self.operational_webhook = OperationalWebhookResource(self.http)
100
+ self.rate_limit = RateLimitResource(self.http)
101
+ self.audit = AuditResource(self.http)
102
+ self.sso = SsoResource(self.http)
103
+ self.custom_domain = CustomDomainResource(self.http)
104
+ self.environment = EnvironmentResource(self.http)
105
+
106
+ def me(self) -> Dict[str, Any]:
107
+ """Get current user profile."""
108
+ return self.http.request("GET", "/v1/auth/me")
@@ -0,0 +1,50 @@
1
+ class HookSniffError(Exception):
2
+ """Base exception for HookSniff SDK."""
3
+ def __init__(self, message: str, status_code: int = None, code: str = None):
4
+ super().__init__(message)
5
+ self.status_code = status_code
6
+ self.code = code
7
+
8
+ class AuthenticationError(HookSniffError):
9
+ """Invalid API key."""
10
+ def __init__(self, message: str = "Invalid API key"):
11
+ super().__init__(message, 401, "UNAUTHORIZED")
12
+
13
+ class NotFoundError(HookSniffError):
14
+ """Resource not found."""
15
+ def __init__(self, message: str = "Resource not found"):
16
+ super().__init__(message, 404, "NOT_FOUND")
17
+
18
+ class RateLimitError(HookSniffError):
19
+ """Rate limited."""
20
+ def __init__(self, message: str = "Rate limited", retry_after: int = 60):
21
+ super().__init__(message, 429, "RATE_LIMITED")
22
+ self.retry_after = retry_after
23
+
24
+ class ValidationError(HookSniffError):
25
+ """Validation failed."""
26
+ def __init__(self, message: str = "Validation failed"):
27
+ super().__init__(message, 400, "BAD_REQUEST")
28
+
29
+ class ServerError(HookSniffError):
30
+ """Internal server error."""
31
+ def __init__(self, message: str = "Internal server error"):
32
+ super().__init__(message, 500, "INTERNAL_ERROR")
33
+
34
+ def map_error(status_code: int, body: dict) -> HookSniffError:
35
+ error = body.get("error", {})
36
+ code = error.get("code", "UNKNOWN")
37
+ detail = error.get("detail") or error.get("message") or "Unknown error"
38
+
39
+ if status_code == 401:
40
+ return AuthenticationError(detail)
41
+ elif status_code == 404:
42
+ return NotFoundError(detail)
43
+ elif status_code == 429:
44
+ return RateLimitError(detail)
45
+ elif status_code in (400, 422):
46
+ return ValidationError(detail)
47
+ elif status_code >= 500:
48
+ return ServerError(detail)
49
+ else:
50
+ return HookSniffError(detail, status_code, code)
@@ -0,0 +1,138 @@
1
+ import json
2
+ import time
3
+ import urllib.request
4
+ import urllib.error
5
+ from typing import Any, Dict, Optional
6
+ from .exceptions import map_error, RateLimitError, HookSniffError
7
+
8
+ DEFAULT_BASE_URL = "https://hooksniff-api-e6ztf3x2ma-ew.a.run.app"
9
+ DEFAULT_TIMEOUT = 30
10
+ DEFAULT_RETRIES = 3
11
+ SDK_VERSION = "0.4.0"
12
+
13
+
14
+ class HttpClient:
15
+ def __init__(self, api_key: str, base_url: str = None, timeout: int = None, retries: int = None, headers: Dict[str, str] = None):
16
+ if not api_key:
17
+ raise ValueError("HookSniff API key is required")
18
+ self.api_key = api_key
19
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
20
+ self.timeout = timeout or DEFAULT_TIMEOUT
21
+ self.retries = retries if retries is not None else DEFAULT_RETRIES
22
+ self.extra_headers = headers or {}
23
+
24
+ def request(self, method: str, path: str, body: Any = None, options: Dict[str, str] = None) -> Any:
25
+ url = f"{self.base_url}{path}"
26
+ headers = {
27
+ "Authorization": f"Bearer {self.api_key}",
28
+ "Content-Type": "application/json",
29
+ "User-Agent": f"hooksniff-sdk/python/{SDK_VERSION}",
30
+ **self.extra_headers,
31
+ }
32
+
33
+ if options and options.get("idempotency_key"):
34
+ headers["Idempotency-Key"] = options["idempotency_key"]
35
+
36
+ last_error = None
37
+
38
+ for attempt in range(self.retries + 1):
39
+ try:
40
+ data = json.dumps(body).encode("utf-8") if body else None
41
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
42
+
43
+ with urllib.request.urlopen(req, timeout=self.timeout) as response:
44
+ response_body = response.read().decode("utf-8")
45
+ if not response_body:
46
+ return None
47
+ return json.loads(response_body)
48
+
49
+ except urllib.error.HTTPError as e:
50
+ try:
51
+ error_body = json.loads(e.read().decode("utf-8"))
52
+ except Exception:
53
+ error_body = {}
54
+
55
+ # Don't retry client errors except 429 and 408
56
+ if 400 <= e.code < 500 and e.code != 429 and e.code != 408:
57
+ raise map_error(e.code, error_body)
58
+
59
+ # Handle rate limiting
60
+ if e.code == 429:
61
+ retry_after = int(e.headers.get("Retry-After", "60"))
62
+ if attempt < self.retries:
63
+ time.sleep(retry_after)
64
+ continue
65
+ raise RateLimitError(error_body.get("error", {}).get("detail", "Rate limited"), retry_after)
66
+
67
+ # Server errors and timeouts - retry
68
+ last_error = map_error(e.code, error_body)
69
+ if attempt < self.retries:
70
+ time.sleep(2 ** attempt + 0.5)
71
+ continue
72
+
73
+ raise last_error
74
+
75
+ except Exception as e:
76
+ last_error = e
77
+ if attempt < self.retries:
78
+ time.sleep(2 ** attempt + 0.5)
79
+ continue
80
+
81
+ raise last_error or Exception("Request failed after retries")
82
+
83
+
84
+ class PaginatedList:
85
+ """Auto-paginating list iterator."""
86
+
87
+ def __init__(self, http: HttpClient, path: str, params: Dict[str, Any] = None, per_page: int = 50):
88
+ self.http = http
89
+ self.path = path
90
+ self.params = params or {}
91
+ self.per_page = per_page
92
+ self.page = 1
93
+ self.items = []
94
+ self.exhausted = False
95
+
96
+ def __iter__(self):
97
+ return self
98
+
99
+ def __next__(self):
100
+ if not self.items and not self.exhausted:
101
+ self._fetch_page()
102
+
103
+ if not self.items:
104
+ raise StopIteration
105
+
106
+ return self.items.pop(0)
107
+
108
+ def _fetch_page(self):
109
+ params = {**self.params, "page": str(self.page), "per_page": str(self.per_page)}
110
+ query = "&".join(f"{k}={v}" for k, v in params.items() if v)
111
+ path = f"{self.path}?{query}" if query else self.path
112
+
113
+ response = self.http.request("GET", path)
114
+
115
+ if isinstance(response, list):
116
+ self.items = response
117
+ if len(self.items) < self.per_page:
118
+ self.exhausted = True
119
+ elif isinstance(response, dict):
120
+ # Handle different response formats
121
+ if "deliveries" in response:
122
+ self.items = response["deliveries"]
123
+ elif "data" in response:
124
+ self.items = response["data"]
125
+ elif "applications" in response:
126
+ self.items = response["applications"]
127
+ elif "endpoints" in response:
128
+ self.items = response["endpoints"]
129
+ else:
130
+ self.items = []
131
+ if not self.items or response.get("has_more") is False:
132
+ self.exhausted = True
133
+
134
+ self.page += 1
135
+
136
+ def all(self) -> list:
137
+ """Collect all items into a list."""
138
+ return list(self)
hooksniff/resources.py ADDED
@@ -0,0 +1,410 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from .http_client import HttpClient, PaginatedList
3
+
4
+
5
+ class ApplicationResource:
6
+ def __init__(self, http: HttpClient):
7
+ self.http = http
8
+
9
+ def create(self, name: str, description: str = None) -> Dict[str, Any]:
10
+ body = {"name": name}
11
+ if description:
12
+ body["description"] = description
13
+ return self.http.request("POST", "/v1/applications", body)
14
+
15
+ def list(self, per_page: int = 50) -> PaginatedList:
16
+ return PaginatedList(self.http, "/v1/applications", per_page=per_page)
17
+
18
+ def get(self, application_id: str) -> Dict[str, Any]:
19
+ return self.http.request("GET", f"/v1/applications/{application_id}")
20
+
21
+ def update(self, application_id: str, **kwargs) -> Dict[str, Any]:
22
+ return self.http.request("PUT", f"/v1/applications/{application_id}", kwargs)
23
+
24
+ def delete(self, application_id: str) -> None:
25
+ self.http.request("DELETE", f"/v1/applications/{application_id}")
26
+
27
+
28
+ class EndpointResource:
29
+ def __init__(self, http: HttpClient):
30
+ self.http = http
31
+
32
+ def create(self, url: str, application_id: str, description: str = None, **kwargs) -> Dict[str, Any]:
33
+ body = {"url": url, "application_id": application_id}
34
+ if description:
35
+ body["description"] = description
36
+ body.update(kwargs)
37
+ return self.http.request("POST", "/v1/endpoints", body)
38
+
39
+ def list(self, per_page: int = 50) -> PaginatedList:
40
+ return PaginatedList(self.http, "/v1/endpoints", per_page=per_page)
41
+
42
+ def get(self, endpoint_id: str) -> Dict[str, Any]:
43
+ return self.http.request("GET", f"/v1/endpoints/{endpoint_id}")
44
+
45
+ def update(self, endpoint_id: str, **kwargs) -> Dict[str, Any]:
46
+ return self.http.request("PUT", f"/v1/endpoints/{endpoint_id}", kwargs)
47
+
48
+ def delete(self, endpoint_id: str) -> None:
49
+ self.http.request("DELETE", f"/v1/endpoints/{endpoint_id}")
50
+
51
+ def rotate_secret(self, endpoint_id: str) -> Dict[str, Any]:
52
+ return self.http.request("POST", f"/v1/endpoints/{endpoint_id}/rotate-secret")
53
+
54
+
55
+ class WebhookResource:
56
+ def __init__(self, http: HttpClient):
57
+ self.http = http
58
+
59
+ def send(self, endpoint_id: str, event: str, data: Dict[str, Any], idempotency_key: str = None) -> Dict[str, Any]:
60
+ body = {"endpoint_id": endpoint_id, "event": event, "data": data}
61
+ options = {"idempotency_key": idempotency_key} if idempotency_key else None
62
+ return self.http.request("POST", "/v1/webhooks", body, options)
63
+
64
+ def send_batch(self, webhooks: List[Dict[str, Any]]) -> Dict[str, Any]:
65
+ return self.http.request("POST", "/v1/webhooks/batch", {"webhooks": webhooks})
66
+
67
+ def list(self, per_page: int = 50, endpoint_id: str = None, status: str = None) -> PaginatedList:
68
+ params = {}
69
+ if endpoint_id:
70
+ params["endpoint_id"] = endpoint_id
71
+ if status:
72
+ params["status"] = status
73
+ return PaginatedList(self.http, "/v1/webhooks", params=params, per_page=per_page)
74
+
75
+ def get(self, webhook_id: str) -> Dict[str, Any]:
76
+ return self.http.request("GET", f"/v1/webhooks/{webhook_id}")
77
+
78
+ def replay(self, webhook_id: str) -> Dict[str, Any]:
79
+ return self.http.request("POST", f"/v1/webhooks/{webhook_id}/replay")
80
+
81
+
82
+ class ApiKeyResource:
83
+ def __init__(self, http: HttpClient):
84
+ self.http = http
85
+
86
+ def list(self) -> List[Dict[str, Any]]:
87
+ return self.http.request("GET", "/v1/api-keys")
88
+
89
+ def create(self, name: str) -> Dict[str, Any]:
90
+ return self.http.request("POST", "/v1/api-keys", {"name": name})
91
+
92
+ def delete(self, api_key_id: str) -> None:
93
+ self.http.request("DELETE", f"/v1/api-keys/{api_key_id}")
94
+
95
+
96
+ class AnalyticsResource:
97
+ def __init__(self, http: HttpClient):
98
+ self.http = http
99
+
100
+ def deliveries(self, range: str = "24h") -> Any:
101
+ return self.http.request("GET", f"/v1/analytics/deliveries?range={range}")
102
+
103
+ def success_rate(self, range: str = "24h") -> Any:
104
+ return self.http.request("GET", f"/v1/analytics/success-rate?range={range}")
105
+
106
+
107
+ class SearchResource:
108
+ def __init__(self, http: HttpClient):
109
+ self.http = http
110
+
111
+ def deliveries(self, query: str, page: int = 1, per_page: int = 20) -> Dict[str, Any]:
112
+ return self.http.request("GET", f"/v1/search?q={query}&page={page}&per_page={per_page}")
113
+
114
+
115
+ class HealthResource:
116
+ def __init__(self, http: HttpClient):
117
+ self.http = http
118
+
119
+ def check(self) -> Dict[str, Any]:
120
+ return self.http.request("GET", "/health")
121
+
122
+ def outbound_ips(self) -> Dict[str, Any]:
123
+ return self.http.request("GET", "/v1/outbound-ips")
124
+
125
+
126
+ class BillingResource:
127
+ def __init__(self, http: HttpClient):
128
+ self.http = http
129
+
130
+ def subscription(self) -> Dict[str, Any]:
131
+ return self.http.request("GET", "/v1/billing/subscription")
132
+
133
+ def upgrade(self, plan: str) -> Dict[str, Any]:
134
+ return self.http.request("POST", "/v1/billing/upgrade", {"plan": plan})
135
+
136
+ def portal(self) -> Dict[str, Any]:
137
+ return self.http.request("POST", "/v1/billing/portal")
138
+
139
+
140
+ class NotificationResource:
141
+ def __init__(self, http: HttpClient):
142
+ self.http = http
143
+
144
+ def list(self, per_page: int = 20) -> Dict[str, Any]:
145
+ return self.http.request("GET", f"/v1/notifications?per_page={per_page}")
146
+
147
+ def get_unread_count(self) -> Dict[str, Any]:
148
+ response = self.http.request("GET", "/v1/notifications/unread-count")
149
+ return {"count": response.get("unread_count", 0)}
150
+
151
+ def mark_read(self, notification_id: str) -> None:
152
+ self.http.request("POST", f"/v1/notifications/{notification_id}/read")
153
+
154
+ def mark_all_read(self) -> None:
155
+ self.http.request("POST", "/v1/notifications/read-all")
156
+
157
+
158
+ class CortexResource:
159
+ def __init__(self, http: HttpClient):
160
+ self.http = http
161
+
162
+ def insights(self) -> List[Dict[str, Any]]:
163
+ response = self.http.request("GET", "/v1/cortex/insights")
164
+ if isinstance(response, dict) and "insights" in response:
165
+ raw = response["insights"]
166
+ result = []
167
+ for row in raw:
168
+ if isinstance(row, list) and len(row) >= 10:
169
+ result.append({
170
+ "id": row[0],
171
+ "customer_id": row[1],
172
+ "type": row[2],
173
+ "title": row[3],
174
+ "description": row[4],
175
+ "severity": row[5],
176
+ "metadata": row[7] if len(row) > 7 else {},
177
+ "created_at": row[9] if len(row) > 9 else None,
178
+ })
179
+ else:
180
+ result.append(row)
181
+ return result
182
+ return response if isinstance(response, list) else []
183
+
184
+ def anomalies(self, endpoint_id: str = None) -> List[Dict[str, Any]]:
185
+ path = "/v1/cortex/anomalies"
186
+ if endpoint_id:
187
+ path += f"?endpoint_id={endpoint_id}"
188
+ return self.http.request("GET", path)
189
+
190
+ def predict(self, endpoint_id: str) -> Dict[str, Any]:
191
+ return self.http.request("GET", f"/v1/cortex/predict/{endpoint_id}")
192
+
193
+ def auto_heal(self, endpoint_id: str) -> Dict[str, Any]:
194
+ return self.http.request("POST", f"/v1/cortex/auto-heal/{endpoint_id}")
195
+
196
+
197
+ class TeamResource:
198
+ def __init__(self, http: HttpClient):
199
+ self.http = http
200
+
201
+ def list(self) -> List[Dict[str, Any]]:
202
+ return self.http.request("GET", "/v1/teams")
203
+
204
+ def create(self, name: str, description: str = None) -> Dict[str, Any]:
205
+ body = {"name": name}
206
+ if description:
207
+ body["description"] = description
208
+ return self.http.request("POST", "/v1/teams", body)
209
+
210
+ def get(self, team_id: str) -> Dict[str, Any]:
211
+ return self.http.request("GET", f"/v1/teams/{team_id}")
212
+
213
+ def delete(self, team_id: str) -> None:
214
+ self.http.request("DELETE", f"/v1/teams/{team_id}")
215
+
216
+
217
+ class AlertResource:
218
+ def __init__(self, http: HttpClient):
219
+ self.http = http
220
+
221
+ def list(self) -> List[Dict[str, Any]]:
222
+ response = self.http.request("GET", "/v1/alerts")
223
+ return response if isinstance(response, list) else response.get("alerts", [])
224
+
225
+ def create(self, name: str, condition: str, threshold: int, channels: List[str]) -> Dict[str, Any]:
226
+ return self.http.request("POST", "/v1/alerts", {
227
+ "name": name, "condition": condition, "threshold": threshold, "channels": channels
228
+ })
229
+
230
+ def get(self, alert_id: str) -> Dict[str, Any]:
231
+ return self.http.request("GET", f"/v1/alerts/{alert_id}")
232
+
233
+ def delete(self, alert_id: str) -> None:
234
+ self.http.request("DELETE", f"/v1/alerts/{alert_id}")
235
+
236
+
237
+ class TemplateResource:
238
+ def __init__(self, http: HttpClient):
239
+ self.http = http
240
+
241
+ def list(self) -> List[Dict[str, Any]]:
242
+ response = self.http.request("GET", "/v1/templates")
243
+ return response if isinstance(response, list) else response.get("templates", [])
244
+
245
+ def get(self, template_id: str) -> Dict[str, Any]:
246
+ return self.http.request("GET", f"/v1/templates/{template_id}")
247
+
248
+
249
+ class SchemaResource:
250
+ def __init__(self, http: HttpClient):
251
+ self.http = http
252
+
253
+ def list(self) -> List[Dict[str, Any]]:
254
+ response = self.http.request("GET", "/v1/schemas")
255
+ return response if isinstance(response, list) else response.get("schemas", [])
256
+
257
+ def create(self, name: str, schema: Dict[str, Any]) -> Dict[str, Any]:
258
+ return self.http.request("POST", "/v1/schemas", {"name": name, "schema": schema})
259
+
260
+ def get(self, schema_id: str) -> Dict[str, Any]:
261
+ return self.http.request("GET", f"/v1/schemas/{schema_id}")
262
+
263
+ def delete(self, schema_id: str) -> None:
264
+ self.http.request("DELETE", f"/v1/schemas/{schema_id}")
265
+
266
+
267
+ class ConnectorResource:
268
+ def __init__(self, http: HttpClient):
269
+ self.http = http
270
+
271
+ def list(self) -> List[Dict[str, Any]]:
272
+ return self.http.request("GET", "/v1/connectors")
273
+
274
+ def get(self, connector_id: str) -> Dict[str, Any]:
275
+ return self.http.request("GET", f"/v1/connectors/{connector_id}")
276
+
277
+ def list_configs(self) -> List[Dict[str, Any]]:
278
+ return self.http.request("GET", "/v1/connectors/configs")
279
+
280
+ def create_config(self, connector_id: str, name: str, config: Dict[str, Any]) -> Dict[str, Any]:
281
+ return self.http.request("POST", "/v1/connectors/configs", {
282
+ "connector_id": connector_id, "name": name, "config": config
283
+ })
284
+
285
+
286
+ class StreamResource:
287
+ def __init__(self, http: HttpClient):
288
+ self.http = http
289
+
290
+ def list_channels(self) -> List[Dict[str, Any]]:
291
+ return self.http.request("GET", "/v1/stream/channels")
292
+
293
+ def create_channel(self, name: str, description: str = None) -> Dict[str, Any]:
294
+ body = {"name": name}
295
+ if description:
296
+ body["description"] = description
297
+ return self.http.request("POST", "/v1/stream/channels", body)
298
+
299
+ def get_channel(self, channel_id: str) -> Dict[str, Any]:
300
+ return self.http.request("GET", f"/v1/stream/channels/{channel_id}")
301
+
302
+ def delete_channel(self, channel_id: str) -> None:
303
+ self.http.request("DELETE", f"/v1/stream/channels/{channel_id}")
304
+
305
+ def publish(self, channel_id: str, event: str, data: Dict[str, Any]) -> Dict[str, Any]:
306
+ return self.http.request("POST", "/v1/stream/publish", {
307
+ "channel_id": channel_id, "event": event, "data": data
308
+ })
309
+
310
+ def list_subscriptions(self) -> List[Dict[str, Any]]:
311
+ return self.http.request("GET", "/v1/stream/subscriptions")
312
+
313
+
314
+ class BackgroundTaskResource:
315
+ def __init__(self, http: HttpClient):
316
+ self.http = http
317
+
318
+ def list(self) -> List[Dict[str, Any]]:
319
+ return self.http.request("GET", "/v1/background-tasks")
320
+
321
+ def get(self, task_id: str) -> Dict[str, Any]:
322
+ return self.http.request("GET", f"/v1/background-tasks/{task_id}")
323
+
324
+ def cancel(self, task_id: str) -> None:
325
+ self.http.request("POST", f"/v1/background-tasks/{task_id}/cancel")
326
+
327
+
328
+ class IntegrationResource:
329
+ def __init__(self, http: HttpClient):
330
+ self.http = http
331
+
332
+ def list(self) -> List[Dict[str, Any]]:
333
+ return self.http.request("GET", "/v1/integrations")
334
+
335
+ def get(self, integration_id: str) -> Dict[str, Any]:
336
+ return self.http.request("GET", f"/v1/integrations/{integration_id}")
337
+
338
+ def delete(self, integration_id: str) -> None:
339
+ self.http.request("DELETE", f"/v1/integrations/{integration_id}")
340
+
341
+
342
+ class ServiceTokenResource:
343
+ def __init__(self, http: HttpClient):
344
+ self.http = http
345
+
346
+ def list(self) -> List[Dict[str, Any]]:
347
+ return self.http.request("GET", "/v1/service-tokens")
348
+
349
+ def create(self, name: str) -> Dict[str, Any]:
350
+ return self.http.request("POST", "/v1/service-tokens", {"name": name})
351
+
352
+ def delete(self, token_id: str) -> None:
353
+ self.http.request("DELETE", f"/v1/service-tokens/{token_id}")
354
+
355
+
356
+ class OperationalWebhookResource:
357
+ def __init__(self, http: HttpClient):
358
+ self.http = http
359
+
360
+ def list(self) -> List[Dict[str, Any]]:
361
+ return self.http.request("GET", "/v1/operational-webhooks")
362
+
363
+ def create(self, url: str, events: List[str]) -> Dict[str, Any]:
364
+ return self.http.request("POST", "/v1/operational-webhooks", {"url": url, "events": events})
365
+
366
+ def get(self, webhook_id: str) -> Dict[str, Any]:
367
+ return self.http.request("GET", f"/v1/operational-webhooks/{webhook_id}")
368
+
369
+ def delete(self, webhook_id: str) -> None:
370
+ self.http.request("DELETE", f"/v1/operational-webhooks/{webhook_id}")
371
+
372
+
373
+ class RateLimitResource:
374
+ def __init__(self, http: HttpClient):
375
+ self.http = http
376
+
377
+ def list(self) -> List[Dict[str, Any]]:
378
+ return self.http.request("GET", "/v1/rate-limits")
379
+
380
+
381
+ class AuditResource:
382
+ def __init__(self, http: HttpClient):
383
+ self.http = http
384
+
385
+ def list(self) -> Dict[str, Any]:
386
+ return self.http.request("GET", "/v1/audit-log")
387
+
388
+
389
+ class SsoResource:
390
+ def __init__(self, http: HttpClient):
391
+ self.http = http
392
+
393
+ def get_config(self) -> List[Dict[str, Any]]:
394
+ return self.http.request("GET", "/v1/sso/config")
395
+
396
+
397
+ class CustomDomainResource:
398
+ def __init__(self, http: HttpClient):
399
+ self.http = http
400
+
401
+ def list(self) -> List[Dict[str, Any]]:
402
+ return self.http.request("GET", "/v1/custom-domains")
403
+
404
+
405
+ class EnvironmentResource:
406
+ def __init__(self, http: HttpClient):
407
+ self.http = http
408
+
409
+ def list(self) -> List[Dict[str, Any]]:
410
+ return self.http.request("GET", "/v1/environments")
hooksniff/webhook.py ADDED
@@ -0,0 +1,85 @@
1
+ import json
2
+ import time
3
+ import hmac
4
+ import hashlib
5
+ import base64
6
+ from typing import Any, Dict, Union
7
+
8
+
9
+ class WebhookVerificationError(Exception):
10
+ """Webhook verification failed."""
11
+ pass
12
+
13
+
14
+ class Webhook:
15
+ """
16
+ Verify and parse incoming webhook payloads.
17
+ Compliant with the Standard Webhooks specification.
18
+
19
+ Usage:
20
+ wh = Webhook("whsec_...")
21
+ event = wh.verify(request.body, request.headers)
22
+ """
23
+
24
+ def __init__(self, secret: str):
25
+ if not secret:
26
+ raise ValueError("Webhook secret is required")
27
+ self.secret = secret
28
+
29
+ def verify(self, payload: Union[str, bytes], headers: Dict[str, str]) -> Any:
30
+ normalized_headers = {k.lower(): v for k, v in headers.items()}
31
+
32
+ msg_id = normalized_headers.get("webhook-id")
33
+ msg_signature = normalized_headers.get("webhook-signature")
34
+ msg_timestamp = normalized_headers.get("webhook-timestamp")
35
+
36
+ if not msg_id or not msg_signature or not msg_timestamp:
37
+ raise WebhookVerificationError(
38
+ "Missing required webhook headers (webhook-id, webhook-signature, webhook-timestamp)"
39
+ )
40
+
41
+ try:
42
+ timestamp = int(msg_timestamp)
43
+ except ValueError:
44
+ raise WebhookVerificationError("Invalid webhook timestamp")
45
+
46
+ now = int(time.time())
47
+ if abs(now - timestamp) > 300:
48
+ raise WebhookVerificationError("Webhook timestamp is too old")
49
+
50
+ content = payload if isinstance(payload, str) else payload.decode("utf-8")
51
+ to_sign = f"{msg_id}.{msg_timestamp}.{content}"
52
+ expected_signature = self._sign(to_sign)
53
+
54
+ signatures = msg_signature.split(" ")
55
+ is_valid = False
56
+ for sig in signatures:
57
+ parts = sig.split(",", 1)
58
+ if len(parts) != 2:
59
+ continue
60
+ version, signature = parts
61
+ if version != "v1":
62
+ continue
63
+
64
+ try:
65
+ if hmac.compare_digest(signature, expected_signature):
66
+ is_valid = True
67
+ break
68
+ except Exception:
69
+ continue
70
+
71
+ if not is_valid:
72
+ raise WebhookVerificationError("Invalid webhook signature")
73
+
74
+ return json.loads(content)
75
+
76
+ def _sign(self, content: str) -> str:
77
+ secret_bytes = self.secret.replace("whsec_", "").encode("utf-8")
78
+ try:
79
+ secret_key = base64.b64decode(secret_bytes)
80
+ except Exception:
81
+ secret_key = secret_bytes
82
+
83
+ return base64.b64encode(
84
+ hmac.new(secret_key, content.encode("utf-8"), hashlib.sha256).digest()
85
+ ).decode("utf-8")
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: hooksniff-python
3
+ Version: 0.4.1
4
+ Summary: Official HookSniff SDK for Python - Webhook infrastructure for developers
5
+ Project-URL: Homepage, https://hooksniff.vercel.app
6
+ Project-URL: Repository, https://github.com/servetarslan02/hooksniff-python
7
+ Author-email: HookSniff <support@hooksniff.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: api,hooksniff,sdk,webhook,webhooks
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+
24
+ # HookSniff Python SDK
25
+
26
+ Official Python SDK for [HookSniff](https://hooksniff.vercel.app) — the webhook infrastructure for developers.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install hooksniff
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```python
37
+ from hooksniff import HookSniff
38
+
39
+ hs = HookSniff("hr_live_...")
40
+
41
+ # Create an application
42
+ app = hs.application.create(name="My App")
43
+
44
+ # Create an endpoint
45
+ ep = hs.endpoint.create(
46
+ url="https://app.com/webhook",
47
+ application_id=app["id"],
48
+ description="Order notifications",
49
+ )
50
+
51
+ # Send a webhook
52
+ delivery = hs.webhook.send(
53
+ endpoint_id=ep["id"],
54
+ event="order.created",
55
+ data={"order_id": "12345", "amount": 99.99},
56
+ )
57
+
58
+ print(delivery["id"]) # "msg_..."
59
+ ```
60
+
61
+ ## Features
62
+
63
+ - **Simple API** — 2 lines to send a webhook
64
+ - **Auto-pagination** — Iterate through all resources automatically
65
+ - **Auto-retry** — Exponential backoff on 429/5xx errors
66
+ - **Idempotency** — Built-in idempotency key support
67
+ - **Webhook verification** — Verify incoming webhooks (Standard Webhooks compliant)
68
+ - **Error handling** — Typed exceptions
69
+ - **27+ Resources** — Application, Endpoint, Webhook, Billing, Cortex, Teams, and more
70
+
71
+ ## Usage
72
+
73
+ ### Applications
74
+
75
+ ```python
76
+ # Create
77
+ app = hs.application.create(name="My App", description="My application")
78
+
79
+ # List with auto-pagination
80
+ for app in hs.application.list():
81
+ print(app["name"])
82
+
83
+ # Get
84
+ app = hs.application.get("app_123")
85
+
86
+ # Update
87
+ app = hs.application.update("app_123", name="New Name")
88
+
89
+ # Delete
90
+ hs.application.delete("app_123")
91
+ ```
92
+
93
+ ### Endpoints
94
+
95
+ ```python
96
+ # Create
97
+ ep = hs.endpoint.create(
98
+ url="https://app.com/webhook",
99
+ application_id="app_123",
100
+ description="Order notifications",
101
+ )
102
+
103
+ # List with auto-pagination
104
+ for ep in hs.endpoint.list():
105
+ print(ep["url"])
106
+
107
+ # Get
108
+ ep = hs.endpoint.get("ep_123")
109
+
110
+ # Update
111
+ ep = hs.endpoint.update("ep_123", url="https://new-url.com/webhook")
112
+
113
+ # Delete
114
+ hs.endpoint.delete("ep_123")
115
+
116
+ # Rotate signing secret
117
+ secret = hs.endpoint.rotate_secret("ep_123")
118
+ print(secret["signing_secret"]) # "whsec_..."
119
+ ```
120
+
121
+ ### Webhooks
122
+
123
+ ```python
124
+ # Send a webhook
125
+ delivery = hs.webhook.send(
126
+ endpoint_id="ep_123",
127
+ event="order.created",
128
+ data={"order_id": "12345", "amount": 99.99},
129
+ )
130
+
131
+ # Send with idempotency key
132
+ delivery = hs.webhook.send(
133
+ endpoint_id="ep_123",
134
+ event="order.created",
135
+ data={},
136
+ idempotency_key="unique-key-123",
137
+ )
138
+
139
+ # Send batch
140
+ result = hs.webhook.send_batch([
141
+ {"endpoint_id": "ep_123", "event": "order.created", "data": {"id": "1"}},
142
+ {"endpoint_id": "ep_123", "event": "order.created", "data": {"id": "2"}},
143
+ ])
144
+
145
+ # List deliveries with auto-pagination
146
+ for d in hs.webhook.list(per_page=10):
147
+ print(d["status"])
148
+
149
+ # Get delivery
150
+ delivery = hs.webhook.get("msg_123")
151
+
152
+ # Replay delivery
153
+ delivery = hs.webhook.replay("msg_123")
154
+ ```
155
+
156
+ ### Webhook Verification
157
+
158
+ ```python
159
+ from hooksniff import Webhook, WebhookVerificationError
160
+
161
+ wh = Webhook("whsec_...")
162
+
163
+ # In your Flask/FastAPI handler:
164
+ def handle_webhook(request):
165
+ try:
166
+ event = wh.verify(request.body, request.headers)
167
+ print(event)
168
+ return "OK", 200
169
+ except WebhookVerificationError:
170
+ return "Invalid signature", 401
171
+ ```
172
+
173
+ ### Error Handling
174
+
175
+ ```python
176
+ from hooksniff import AuthenticationError, NotFoundError, RateLimitError
177
+
178
+ try:
179
+ hs.endpoint.get("invalid_id")
180
+ except NotFoundError:
181
+ print("Endpoint not found")
182
+ except AuthenticationError:
183
+ print("Invalid API key")
184
+ except RateLimitError as e:
185
+ print(f"Rate limited, retry after {e.retry_after}s")
186
+ ```
187
+
188
+ ### All Resources
189
+
190
+ ```python
191
+ # API Keys
192
+ hs.api_key.list()
193
+ hs.api_key.create(name="Production Key")
194
+ hs.api_key.delete("key_123")
195
+
196
+ # Analytics
197
+ hs.analytics.deliveries(range="24h")
198
+ hs.analytics.success_rate(range="7d")
199
+
200
+ # Billing
201
+ hs.billing.subscription()
202
+ hs.billing.upgrade(plan="pro")
203
+ hs.billing.portal()
204
+
205
+ # Teams
206
+ hs.team.list()
207
+ hs.team.create(name="Engineering")
208
+
209
+ # Cortex AI
210
+ hs.cortex.insights()
211
+ hs.cortex.anomalies(endpoint_id="ep_123")
212
+ hs.cortex.predict("ep_123")
213
+ hs.cortex.auto_heal("ep_123")
214
+
215
+ # Notifications
216
+ hs.notification.list()
217
+ hs.notification.get_unread_count()
218
+ hs.notification.mark_read("notif_123")
219
+ hs.notification.mark_all_read()
220
+
221
+ # Templates
222
+ hs.template.list()
223
+ hs.template.get("tmpl_123")
224
+
225
+ # Schemas
226
+ hs.schema.list()
227
+ hs.schema.create(name="Order Schema", schema={...})
228
+ hs.schema.validate("schema_123", {"order_id": "123"})
229
+
230
+ # Alerts
231
+ hs.alert.list()
232
+ hs.alert.create(name="...", condition="failure_rate", threshold=10, channels=["email"])
233
+
234
+ # Connectors
235
+ hs.connector.list()
236
+ hs.connector.list_configs()
237
+
238
+ # Stream
239
+ hs.stream.list_channels()
240
+ hs.stream.publish(channel_id="ch_123", event="test", data={...})
241
+
242
+ # Background Tasks
243
+ hs.background_task.list()
244
+ hs.background_task.get("task_123")
245
+
246
+ # Integrations
247
+ hs.integration.list()
248
+
249
+ # Service Tokens
250
+ hs.service_token.list()
251
+
252
+ # Operational Webhooks
253
+ hs.operational_webhook.list()
254
+
255
+ # Search
256
+ hs.search.deliveries("order.created")
257
+
258
+ # Health
259
+ hs.health.check()
260
+ hs.health.outbound_ips()
261
+
262
+ # User
263
+ hs.me()
264
+ ```
265
+
266
+ ## Configuration
267
+
268
+ ```python
269
+ hs = HookSniff(
270
+ "hr_live_...",
271
+ base_url="https://your-instance.com", # Custom API URL
272
+ timeout=30, # Request timeout (seconds)
273
+ retries=3, # Max retries on 5xx/429
274
+ headers={"X-Custom": "value"}, # Custom headers
275
+ )
276
+ ```
277
+
278
+ ## Requirements
279
+
280
+ - Python 3.8+
281
+
282
+ ## License
283
+
284
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,10 @@
1
+ hooksniff/__init__.py,sha256=-umMsNLbD7dXy5F0lwRSdCGszw9DrsJUN9moD1puGug,917
2
+ hooksniff/client.py,sha256=2fej_BEzzsMChLvpKZgu-yhuMXO_CfHEr6eUS1rATe8,3292
3
+ hooksniff/exceptions.py,sha256=6gMV_z99GPo50eoQvTgJ4TMdKrijJsGdI6o6c2ZshdE,1857
4
+ hooksniff/http_client.py,sha256=PJjtnmbTLhP37dXmayn2notznqUr3J9LEMmJUa3akUc,5038
5
+ hooksniff/resources.py,sha256=s72ROxldkvhqKvX8s7kiKkBymP_UXmAc1-HB1mEjGLU,14648
6
+ hooksniff/webhook.py,sha256=fRiCwe7iO3VJCs5LMPuJqVg90ahiwP9x4Rfl8hX5uP4,2680
7
+ hooksniff_python-0.4.1.dist-info/METADATA,sha256=1zSa9rZ6OgfcsvFCY8i3M_nyyPWBE82IGsVcd3-lojE,6355
8
+ hooksniff_python-0.4.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
9
+ hooksniff_python-0.4.1.dist-info/licenses/LICENSE,sha256=vUMb56ZR5dltGS5hpFwKXc8P5mMh4E8S1zwf3MIVAZE,1066
10
+ hooksniff_python-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HookSniff
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.