hooksniff 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hooksniff/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ HookSniff Python SDK - Enterprise webhook delivery service client.
3
+ """
4
+
5
+ from .client import HookSniffClient
6
+ from .models import (
7
+ Endpoint,
8
+ Delivery,
9
+ DeliveryAttempt,
10
+ DeliveryList,
11
+ BatchResult,
12
+ Stats,
13
+ RetryPolicy,
14
+ OrderCreatedPayload,
15
+ OrderCompletedPayload,
16
+ PaymentFailedPayload,
17
+ PaymentSucceededPayload,
18
+ UserRegisteredPayload,
19
+ UserUpdatedPayload,
20
+ InvoiceCreatedPayload,
21
+ )
22
+ from .exceptions import (
23
+ HookSniffError,
24
+ AuthenticationError,
25
+ NotFoundError,
26
+ RateLimitError,
27
+ ValidationError,
28
+ PayloadTooLargeError,
29
+ )
30
+ from .utils import verify_signature, verify_webhook_signature, WebhookHandler
31
+ from .verify import (
32
+ WebhookEvent,
33
+ WebhookVerifier,
34
+ verify_webhook,
35
+ verify_webhook_request,
36
+ )
37
+
38
+ __version__ = "0.1.0"
39
+ __all__ = [
40
+ "HookSniffClient",
41
+ # Models
42
+ "Endpoint",
43
+ "Delivery",
44
+ "DeliveryAttempt",
45
+ "DeliveryList",
46
+ "BatchResult",
47
+ "Stats",
48
+ # Webhook payload types
49
+ "OrderCreatedPayload",
50
+ "OrderCompletedPayload",
51
+ "PaymentFailedPayload",
52
+ "PaymentSucceededPayload",
53
+ "UserRegisteredPayload",
54
+ "UserUpdatedPayload",
55
+ "InvoiceCreatedPayload",
56
+ # Exceptions
57
+ "HookSniffError",
58
+ "AuthenticationError",
59
+ "NotFoundError",
60
+ "RateLimitError",
61
+ "ValidationError",
62
+ "PayloadTooLargeError",
63
+ # Legacy webhook verification (utils)
64
+ "verify_signature",
65
+ "verify_webhook_signature",
66
+ "WebhookHandler",
67
+ # Standard Webhooks verification (verify)
68
+ "WebhookEvent",
69
+ "WebhookVerifier",
70
+ "verify_webhook",
71
+ "verify_webhook_request",
72
+ ]
hooksniff/client.py ADDED
@@ -0,0 +1,247 @@
1
+ """HookSniff API client."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ from urllib.parse import urljoin
5
+
6
+ import requests
7
+
8
+ from .exceptions import (
9
+ AuthenticationError,
10
+ HookSniffError,
11
+ NotFoundError,
12
+ PayloadTooLargeError,
13
+ RateLimitError,
14
+ ValidationError,
15
+ )
16
+ from .models import (
17
+ BatchResult,
18
+ Delivery,
19
+ DeliveryAttempt,
20
+ DeliveryList,
21
+ Endpoint,
22
+ RetryPolicy,
23
+ Stats,
24
+ )
25
+
26
+
27
+ class _EndpointsResource:
28
+ """Endpoints CRUD operations."""
29
+
30
+ def __init__(self, client: "HookSniffClient"):
31
+ self._client = client
32
+
33
+ def create(
34
+ self,
35
+ url: str,
36
+ description: Optional[str] = None,
37
+ retry_policy: Optional[RetryPolicy] = None,
38
+ ) -> Endpoint:
39
+ """Create a new webhook endpoint."""
40
+ data: Dict[str, Any] = {"url": url}
41
+ if description is not None:
42
+ data["description"] = description
43
+ if retry_policy is not None:
44
+ data["retry_policy"] = retry_policy.to_dict()
45
+
46
+ resp = self._client._request("POST", "/endpoints", json=data)
47
+ return Endpoint.from_dict(resp)
48
+
49
+ def get(self, endpoint_id: str) -> Endpoint:
50
+ """Get an endpoint by ID."""
51
+ resp = self._client._request("GET", f"/endpoints/{endpoint_id}")
52
+ return Endpoint.from_dict(resp)
53
+
54
+ def list(self, page: int = 1, per_page: int = 20) -> Dict[str, Any]:
55
+ """List endpoints with pagination."""
56
+ params = {"page": page, "per_page": per_page}
57
+ resp = self._client._request("GET", "/endpoints", params=params)
58
+ return {
59
+ "endpoints": [Endpoint.from_dict(e) for e in (resp.get("endpoints", resp) if isinstance(resp, dict) else resp)],
60
+ "total": resp.get("total", 0) if isinstance(resp, dict) else len(resp),
61
+ "page": resp.get("page", page) if isinstance(resp, dict) else page,
62
+ "per_page": resp.get("per_page", per_page) if isinstance(resp, dict) else per_page,
63
+ }
64
+
65
+ def delete(self, endpoint_id: str) -> bool:
66
+ """Delete an endpoint."""
67
+ resp = self._client._request("DELETE", f"/endpoints/{endpoint_id}")
68
+ return resp.get("deleted", False)
69
+
70
+ def rotate_secret(self, endpoint_id: str) -> Dict[str, Any]:
71
+ """Rotate the signing secret for an endpoint."""
72
+ return self._client._request("POST", f"/endpoints/{endpoint_id}/rotate-secret")
73
+
74
+
75
+ class _WebhooksResource:
76
+ """Webhooks operations."""
77
+
78
+ def __init__(self, client: "HookSniffClient"):
79
+ self._client = client
80
+
81
+ def send(
82
+ self,
83
+ endpoint_id: str,
84
+ event: Optional[str] = None,
85
+ data: Optional[Dict[str, Any]] = None,
86
+ ) -> Delivery:
87
+ """Send a webhook."""
88
+ payload: Dict[str, Any] = {"endpoint_id": endpoint_id, "data": data or {}}
89
+ if event is not None:
90
+ payload["event"] = event
91
+
92
+ resp = self._client._request("POST", "/webhooks", json=payload)
93
+ return Delivery.from_dict(resp)
94
+
95
+ def get(self, delivery_id: str) -> Delivery:
96
+ """Get a delivery by ID."""
97
+ resp = self._client._request("GET", f"/webhooks/{delivery_id}")
98
+ return Delivery.from_dict(resp)
99
+
100
+ def list(
101
+ self,
102
+ status: Optional[str] = None,
103
+ page: int = 1,
104
+ per_page: int = 20,
105
+ ) -> DeliveryList:
106
+ """List deliveries with optional filters."""
107
+ params: Dict[str, Any] = {"page": page, "per_page": per_page}
108
+ if status is not None:
109
+ params["status"] = status
110
+
111
+ resp = self._client._request("GET", "/webhooks", params=params)
112
+ return DeliveryList.from_dict(resp)
113
+
114
+ def replay(self, delivery_id: str) -> Delivery:
115
+ """Replay a webhook delivery."""
116
+ resp = self._client._request("POST", f"/webhooks/{delivery_id}/replay")
117
+ return Delivery.from_dict(resp)
118
+
119
+ def batch(self, webhooks: List[Dict[str, Any]]) -> BatchResult:
120
+ """Send multiple webhooks in a batch."""
121
+ resp = self._client._request(
122
+ "POST", "/webhooks/batch", json={"webhooks": webhooks}
123
+ )
124
+ return BatchResult.from_dict(resp)
125
+
126
+ def attempts(self, delivery_id: str) -> List[DeliveryAttempt]:
127
+ """Get delivery attempts for a webhook."""
128
+ resp = self._client._request("GET", f"/webhooks/{delivery_id}/attempts")
129
+ return [DeliveryAttempt.from_dict(a) for a in resp]
130
+
131
+ def export(
132
+ self,
133
+ format: str = "json",
134
+ status: Optional[str] = None,
135
+ date_from: Optional[str] = None,
136
+ date_to: Optional[str] = None,
137
+ ) -> Any:
138
+ """Export webhook logs."""
139
+ params: Dict[str, Any] = {"format": format}
140
+ if status is not None:
141
+ params["status"] = status
142
+ if date_from is not None:
143
+ params["date_from"] = date_from
144
+ if date_to is not None:
145
+ params["date_to"] = date_to
146
+
147
+ resp = self._client._request("GET", "/webhooks/export", params=params)
148
+ return resp
149
+
150
+
151
+
152
+ class HookSniffClient:
153
+ """
154
+ HookSniff API client.
155
+
156
+ Usage:
157
+ from hooksniff import HookSniffClient
158
+
159
+ client = HookSniffClient(api_key="hr_live_...")
160
+
161
+ # Create endpoint
162
+ endpoint = client.endpoints.create(url="https://myapp.com/webhook")
163
+
164
+ # Send webhook
165
+ delivery = client.webhooks.send(
166
+ endpoint_id=endpoint.id,
167
+ event="order.created",
168
+ data={"order_id": "12345"}
169
+ )
170
+ """
171
+
172
+ def __init__(
173
+ self,
174
+ api_key: str,
175
+ base_url: str = "https://hooksniff-api-1046140057667.europe-west1.run.app/v1",
176
+ timeout: int = 30,
177
+ ):
178
+ self._api_key = api_key
179
+ self._base_url = base_url.rstrip("/")
180
+ self._timeout = timeout
181
+ self._session = requests.Session()
182
+ self._session.headers.update(
183
+ {
184
+ "Authorization": f"Bearer {api_key}",
185
+ "Content-Type": "application/json",
186
+ "User-Agent": "hooksniff-python/0.1.0",
187
+ }
188
+ )
189
+
190
+ self.endpoints = _EndpointsResource(self)
191
+ self.webhooks = _WebhooksResource(self)
192
+
193
+ def _request(
194
+ self,
195
+ method: str,
196
+ path: str,
197
+ json: Optional[Dict[str, Any]] = None,
198
+ params: Optional[Dict[str, Any]] = None,
199
+ ) -> Any:
200
+ """Make an API request."""
201
+ url = urljoin(self._base_url + "/", path.lstrip("/"))
202
+
203
+ try:
204
+ response = self._session.request(
205
+ method=method,
206
+ url=url,
207
+ json=json,
208
+ params=params,
209
+ timeout=self._timeout,
210
+ )
211
+ except requests.RequestException as e:
212
+ raise HookSniffError(f"Request failed: {e}") from e
213
+
214
+ if response.status_code == 200:
215
+ # Handle CSV export
216
+ content_type = response.headers.get("content-type", "")
217
+ if "text/csv" in content_type:
218
+ return response.text
219
+ return response.json()
220
+
221
+ # Handle errors
222
+ try:
223
+ error_body = response.json()
224
+ message = error_body.get("error", {}).get("message", response.text)
225
+ except (ValueError, KeyError):
226
+ message = response.text or f"HTTP {response.status_code}"
227
+
228
+ if response.status_code == 400:
229
+ raise ValidationError(message)
230
+ elif response.status_code == 401:
231
+ raise AuthenticationError(message)
232
+ elif response.status_code == 404:
233
+ raise NotFoundError(message)
234
+ elif response.status_code == 413:
235
+ raise PayloadTooLargeError(message)
236
+ elif response.status_code == 429:
237
+ raise RateLimitError(message)
238
+ else:
239
+ raise HookSniffError(
240
+ message,
241
+ status_code=response.status_code,
242
+ )
243
+
244
+ def get_stats(self) -> Stats:
245
+ """Get delivery statistics."""
246
+ resp = self._request("GET", "/stats")
247
+ return Stats.from_dict(resp)
@@ -0,0 +1,46 @@
1
+ """Custom exceptions for HookSniff SDK."""
2
+
3
+
4
+ class HookSniffError(Exception):
5
+ """Base exception for all HookSniff errors."""
6
+
7
+ def __init__(self, message: str, status_code: int = None, error_code: str = None):
8
+ self.message = message
9
+ self.status_code = status_code
10
+ self.error_code = error_code
11
+ super().__init__(self.message)
12
+
13
+
14
+ class AuthenticationError(HookSniffError):
15
+ """Raised when the API key is invalid or missing."""
16
+
17
+ def __init__(self, message: str = "Unauthorized: invalid or missing API key"):
18
+ super().__init__(message, status_code=401, error_code="UNAUTHORIZED")
19
+
20
+
21
+ class NotFoundError(HookSniffError):
22
+ """Raised when a resource is not found."""
23
+
24
+ def __init__(self, message: str = "Resource not found"):
25
+ super().__init__(message, status_code=404, error_code="NOT_FOUND")
26
+
27
+
28
+ class RateLimitError(HookSniffError):
29
+ """Raised when rate limit is exceeded."""
30
+
31
+ def __init__(self, message: str = "Rate limit exceeded"):
32
+ super().__init__(message, status_code=429, error_code="RATE_LIMIT_EXCEEDED")
33
+
34
+
35
+ class ValidationError(HookSniffError):
36
+ """Raised when the request is invalid."""
37
+
38
+ def __init__(self, message: str = "Bad request"):
39
+ super().__init__(message, status_code=400, error_code="BAD_REQUEST")
40
+
41
+
42
+ class PayloadTooLargeError(HookSniffError):
43
+ """Raised when the webhook payload exceeds the maximum size."""
44
+
45
+ def __init__(self, message: str = "Payload too large"):
46
+ super().__init__(message, status_code=413, error_code="PAYLOAD_TOO_LARGE")