webhooks-cc 0.1.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ .venv/
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mads Sauer
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,154 @@
1
+ Metadata-Version: 2.5
2
+ Name: webhooks-cc
3
+ Version: 0.1.0
4
+ Summary: Python SDK for webhooks.cc: capture, wait for, sign and verify webhooks and test emails
5
+ Project-URL: Homepage, https://webhooks.cc
6
+ Project-URL: Documentation, https://github.com/kroqdotdev/webhooks-cc/tree/main/packages/sdk-python#readme
7
+ Project-URL: Repository, https://github.com/kroqdotdev/webhooks-cc
8
+ Author: webhooks.cc
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: email,github,pytest,signature,stripe,testing,webhooks
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx<1,>=0.27
21
+ Provides-Extra: crypto
22
+ Requires-Dist: cryptography>=42; extra == 'crypto'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # webhooks-cc
26
+
27
+ Python SDK for [webhooks.cc](https://webhooks.cc): capture webhooks and email in your tests, wait for them, send signed webhooks to your own handlers, and verify provider signatures.
28
+
29
+ ```bash
30
+ pip install webhooks-cc
31
+ pip install "webhooks-cc[crypto]" # also verify Discord (Ed25519) and PayPal (RSA) signatures
32
+ ```
33
+
34
+ Python 3.10 or later. Get an API key at [webhooks.cc/account/api-keys](https://webhooks.cc/account/api-keys) and set `WHK_API_KEY`, or pass `api_key=`.
35
+
36
+ ## Wait for a webhook
37
+
38
+ ```python
39
+ from webhooks_cc import WebhooksCC, match_body_path
40
+
41
+ with WebhooksCC() as client:
42
+ endpoint = client.endpoints.create(name="checkout test")
43
+
44
+ trigger_checkout(webhook_url=endpoint.url)
45
+
46
+ request = client.requests.wait_for(
47
+ endpoint.slug,
48
+ timeout="30s",
49
+ match=match_body_path("type", "checkout.session.completed"),
50
+ )
51
+ assert request.json()["data"]["object"]["amount_total"] == 2000
52
+
53
+ client.endpoints.delete(endpoint.slug)
54
+ ```
55
+
56
+ Durations take `"500ms"`, `"30s"`, `"5m"`, `"2h"`, `"7d"`, a `timedelta`, or a number of milliseconds (the unit every webhooks.cc SDK uses, so prefer a string where you mean seconds). Times such as `since` and `Request.received_at` are milliseconds since the epoch.
57
+
58
+ Matchers combine with `match_all()` and `match_any()`, and any function that takes a `Request` and returns a bool works too: `match_method`, `match_path` (globs with `*` and `**`), `match_header`, `match_query_param`, `match_body_path`, `match_body_subset`, `match_content_type`, `match_verified`, `match_unverified` and `match_email`.
59
+
60
+ ## pytest
61
+
62
+ Installing the package adds two fixtures. Tests that use them are skipped when `WHK_API_KEY` is not set.
63
+
64
+ ```python
65
+ def test_sends_a_receipt_webhook(webhooks_cc, webhooks_cc_endpoint):
66
+ place_order(webhook_url=webhooks_cc_endpoint.url)
67
+
68
+ request = webhooks_cc.requests.wait_for(webhooks_cc_endpoint.slug, timeout="30s")
69
+ assert request.method == "POST"
70
+ ```
71
+
72
+ `webhooks_cc` is a client for the session; `webhooks_cc_endpoint` is a new endpoint for each test, deleted afterwards. Outside pytest, `temporary_endpoint(client)` is the same as a context manager, and `capture_during(client, action)` runs `action(endpoint)` and returns what it sent.
73
+
74
+ ## Test email: codes and links
75
+
76
+ Every endpoint receives email at `{slug}@mailhooks.cc`, and at `{slug}+{tag}@mailhooks.cc` for any tag. Use a fresh tag per test run to find your own email. webhooks.cc finds the one-time codes and links in each email, the same ones its dashboard shows.
77
+
78
+ ```python
79
+ import uuid
80
+ from webhooks_cc import extract_code, extract_link
81
+
82
+ run = f"signup-{uuid.uuid4().hex[:8]}"
83
+ sign_up(email=client.emails.address(endpoint, run))
84
+
85
+ email = client.emails.wait_for(endpoint.slug, tag=run, subject="Confirm your email")
86
+ code = extract_code(email) # "482913"
87
+ link = extract_link(email) # "https://app.example.com/confirm?token=..."
88
+ ```
89
+
90
+ `email.email` holds the parsed message: `subject`, `from_`, `to`, `text`, `html`, attachments, and the SPF, DKIM and DMARC results.
91
+
92
+ ## Send signed webhooks to your handler
93
+
94
+ Test your own webhook handler, on localhost or anywhere else, with requests signed the way the provider signs them:
95
+
96
+ ```python
97
+ response = client.send_to(
98
+ "http://localhost:8000/webhooks/stripe",
99
+ provider="stripe",
100
+ secret="whsec_test_secret",
101
+ template="checkout.session.completed",
102
+ )
103
+ assert response.status_code == 200
104
+ ```
105
+
106
+ Leave out `body` to send the provider's sample payload, or pass your own: a string or bytes go as they are, anything else as JSON. `timestamp` (seconds) and `message_id` make the signature repeatable. `build_request()` returns the method, URL, headers and body without sending. Redirects are not followed, so signed requests only ever reach the host you named. `client.endpoints.send_template(slug, ...)` sends the same to a webhooks.cc endpoint. `TEMPLATE_PROVIDERS` lists the 34 providers.
107
+
108
+ ## Verify signatures
109
+
110
+ ```python
111
+ from webhooks_cc import verify_signature
112
+
113
+ valid = verify_signature(
114
+ "stripe",
115
+ body=request.body, # the exact bytes or text received
116
+ headers=request.headers, # any mapping; names match in any case
117
+ secret="whsec_...",
118
+ )
119
+ ```
120
+
121
+ Twilio, Square and HubSpot also need `url`; HubSpot also `method`. Discord takes `public_key`. One function per provider is in `webhooks_cc.verify`.
122
+
123
+ When webhooks.cc forwards requests or email to your server, check that they came from it with the endpoint's forwarding secret (`client.forwarding.secret(slug)`):
124
+
125
+ ```python
126
+ from webhooks_cc import WebhookVerificationError, verify_forwarded_email
127
+
128
+ try:
129
+ event = verify_forwarded_email(body, headers, secret)
130
+ except WebhookVerificationError as error:
131
+ return 400 if error.code == "invalid_payload" else 401
132
+ print(event["data"]["subject"])
133
+ ```
134
+
135
+ `verify_forwarded_request()` checks a request forwarded as JSON, and `verify_relayed_request()` one relayed as it was received.
136
+
137
+ ## Stream requests as they arrive
138
+
139
+ ```python
140
+ for request in client.requests.subscribe(endpoint.slug, timeout="10m", reconnect=True):
141
+ print(request.method, request.path)
142
+ ```
143
+
144
+ ## Errors
145
+
146
+ API errors raise `WebhooksCCError` with a `status_code`, or one of its subclasses: `UnauthorizedError` (401), `NotFoundError` (404), `RateLimitError` (429, with `retry_after`) and `TimeoutError`, which is also Python's built-in `TimeoutError`. Network failures raise httpx's exceptions. Retries are off unless you pass `retry=RetryOptions(max_attempts=3)`.
147
+
148
+ ## Same behaviour as the TypeScript SDK
149
+
150
+ This SDK passes the same conformance vectors as [`@webhooks-cc/sdk`](https://www.npmjs.com/package/@webhooks-cc/sdk): signing, signature verification, forwarding signatures, matchers and durations. They live in [`packages/sdk/conformance`](https://github.com/kroqdotdev/webhooks-cc/tree/main/packages/sdk/conformance).
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,130 @@
1
+ # webhooks-cc
2
+
3
+ Python SDK for [webhooks.cc](https://webhooks.cc): capture webhooks and email in your tests, wait for them, send signed webhooks to your own handlers, and verify provider signatures.
4
+
5
+ ```bash
6
+ pip install webhooks-cc
7
+ pip install "webhooks-cc[crypto]" # also verify Discord (Ed25519) and PayPal (RSA) signatures
8
+ ```
9
+
10
+ Python 3.10 or later. Get an API key at [webhooks.cc/account/api-keys](https://webhooks.cc/account/api-keys) and set `WHK_API_KEY`, or pass `api_key=`.
11
+
12
+ ## Wait for a webhook
13
+
14
+ ```python
15
+ from webhooks_cc import WebhooksCC, match_body_path
16
+
17
+ with WebhooksCC() as client:
18
+ endpoint = client.endpoints.create(name="checkout test")
19
+
20
+ trigger_checkout(webhook_url=endpoint.url)
21
+
22
+ request = client.requests.wait_for(
23
+ endpoint.slug,
24
+ timeout="30s",
25
+ match=match_body_path("type", "checkout.session.completed"),
26
+ )
27
+ assert request.json()["data"]["object"]["amount_total"] == 2000
28
+
29
+ client.endpoints.delete(endpoint.slug)
30
+ ```
31
+
32
+ Durations take `"500ms"`, `"30s"`, `"5m"`, `"2h"`, `"7d"`, a `timedelta`, or a number of milliseconds (the unit every webhooks.cc SDK uses, so prefer a string where you mean seconds). Times such as `since` and `Request.received_at` are milliseconds since the epoch.
33
+
34
+ Matchers combine with `match_all()` and `match_any()`, and any function that takes a `Request` and returns a bool works too: `match_method`, `match_path` (globs with `*` and `**`), `match_header`, `match_query_param`, `match_body_path`, `match_body_subset`, `match_content_type`, `match_verified`, `match_unverified` and `match_email`.
35
+
36
+ ## pytest
37
+
38
+ Installing the package adds two fixtures. Tests that use them are skipped when `WHK_API_KEY` is not set.
39
+
40
+ ```python
41
+ def test_sends_a_receipt_webhook(webhooks_cc, webhooks_cc_endpoint):
42
+ place_order(webhook_url=webhooks_cc_endpoint.url)
43
+
44
+ request = webhooks_cc.requests.wait_for(webhooks_cc_endpoint.slug, timeout="30s")
45
+ assert request.method == "POST"
46
+ ```
47
+
48
+ `webhooks_cc` is a client for the session; `webhooks_cc_endpoint` is a new endpoint for each test, deleted afterwards. Outside pytest, `temporary_endpoint(client)` is the same as a context manager, and `capture_during(client, action)` runs `action(endpoint)` and returns what it sent.
49
+
50
+ ## Test email: codes and links
51
+
52
+ Every endpoint receives email at `{slug}@mailhooks.cc`, and at `{slug}+{tag}@mailhooks.cc` for any tag. Use a fresh tag per test run to find your own email. webhooks.cc finds the one-time codes and links in each email, the same ones its dashboard shows.
53
+
54
+ ```python
55
+ import uuid
56
+ from webhooks_cc import extract_code, extract_link
57
+
58
+ run = f"signup-{uuid.uuid4().hex[:8]}"
59
+ sign_up(email=client.emails.address(endpoint, run))
60
+
61
+ email = client.emails.wait_for(endpoint.slug, tag=run, subject="Confirm your email")
62
+ code = extract_code(email) # "482913"
63
+ link = extract_link(email) # "https://app.example.com/confirm?token=..."
64
+ ```
65
+
66
+ `email.email` holds the parsed message: `subject`, `from_`, `to`, `text`, `html`, attachments, and the SPF, DKIM and DMARC results.
67
+
68
+ ## Send signed webhooks to your handler
69
+
70
+ Test your own webhook handler, on localhost or anywhere else, with requests signed the way the provider signs them:
71
+
72
+ ```python
73
+ response = client.send_to(
74
+ "http://localhost:8000/webhooks/stripe",
75
+ provider="stripe",
76
+ secret="whsec_test_secret",
77
+ template="checkout.session.completed",
78
+ )
79
+ assert response.status_code == 200
80
+ ```
81
+
82
+ Leave out `body` to send the provider's sample payload, or pass your own: a string or bytes go as they are, anything else as JSON. `timestamp` (seconds) and `message_id` make the signature repeatable. `build_request()` returns the method, URL, headers and body without sending. Redirects are not followed, so signed requests only ever reach the host you named. `client.endpoints.send_template(slug, ...)` sends the same to a webhooks.cc endpoint. `TEMPLATE_PROVIDERS` lists the 34 providers.
83
+
84
+ ## Verify signatures
85
+
86
+ ```python
87
+ from webhooks_cc import verify_signature
88
+
89
+ valid = verify_signature(
90
+ "stripe",
91
+ body=request.body, # the exact bytes or text received
92
+ headers=request.headers, # any mapping; names match in any case
93
+ secret="whsec_...",
94
+ )
95
+ ```
96
+
97
+ Twilio, Square and HubSpot also need `url`; HubSpot also `method`. Discord takes `public_key`. One function per provider is in `webhooks_cc.verify`.
98
+
99
+ When webhooks.cc forwards requests or email to your server, check that they came from it with the endpoint's forwarding secret (`client.forwarding.secret(slug)`):
100
+
101
+ ```python
102
+ from webhooks_cc import WebhookVerificationError, verify_forwarded_email
103
+
104
+ try:
105
+ event = verify_forwarded_email(body, headers, secret)
106
+ except WebhookVerificationError as error:
107
+ return 400 if error.code == "invalid_payload" else 401
108
+ print(event["data"]["subject"])
109
+ ```
110
+
111
+ `verify_forwarded_request()` checks a request forwarded as JSON, and `verify_relayed_request()` one relayed as it was received.
112
+
113
+ ## Stream requests as they arrive
114
+
115
+ ```python
116
+ for request in client.requests.subscribe(endpoint.slug, timeout="10m", reconnect=True):
117
+ print(request.method, request.path)
118
+ ```
119
+
120
+ ## Errors
121
+
122
+ API errors raise `WebhooksCCError` with a `status_code`, or one of its subclasses: `UnauthorizedError` (401), `NotFoundError` (404), `RateLimitError` (429, with `retry_after`) and `TimeoutError`, which is also Python's built-in `TimeoutError`. Network failures raise httpx's exceptions. Retries are off unless you pass `retry=RetryOptions(max_attempts=3)`.
123
+
124
+ ## Same behaviour as the TypeScript SDK
125
+
126
+ This SDK passes the same conformance vectors as [`@webhooks-cc/sdk`](https://www.npmjs.com/package/@webhooks-cc/sdk): signing, signature verification, forwarding signatures, matchers and durations. They live in [`packages/sdk/conformance`](https://github.com/kroqdotdev/webhooks-cc/tree/main/packages/sdk/conformance).
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "webhooks-cc"
3
+ version = "0.1.0"
4
+ description = "Python SDK for webhooks.cc: capture, wait for, sign and verify webhooks and test emails"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ authors = [{ name = "webhooks.cc" }]
10
+ keywords = ["webhooks", "testing", "stripe", "github", "signature", "email", "pytest"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Framework :: Pytest",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Topic :: Software Development :: Testing",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = ["httpx>=0.27,<1"]
21
+
22
+ [project.optional-dependencies]
23
+ # Discord (Ed25519) and PayPal (RSA) signatures need public-key crypto.
24
+ crypto = ["cryptography>=42"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://webhooks.cc"
28
+ Documentation = "https://github.com/kroqdotdev/webhooks-cc/tree/main/packages/sdk-python#readme"
29
+ Repository = "https://github.com/kroqdotdev/webhooks-cc"
30
+
31
+ [project.entry-points.pytest11]
32
+ webhooks_cc = "webhooks_cc.pytest_plugin"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "cryptography>=42",
37
+ "mypy>=1.13",
38
+ "pytest>=8",
39
+ "ruff>=0.8",
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["hatchling>=1.27"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/webhooks_cc"]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ # The tests read packages/sdk/conformance, which only the repository has.
51
+ include = ["src", "README.md", "LICENSE"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ addopts = "-p no:cacheprovider"
56
+
57
+ [tool.ruff]
58
+ line-length = 100
59
+ target-version = "py310"
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
63
+
64
+ [tool.mypy]
65
+ python_version = "3.10"
66
+ strict = true
67
+ files = ["src"]
@@ -0,0 +1,144 @@
1
+ """webhooks.cc SDK for Python.
2
+
3
+ Capture webhooks and email in tests, wait for them, send signed webhooks to
4
+ your own handlers, and verify provider and forwarding signatures.
5
+
6
+ .. code-block:: python
7
+
8
+ from webhooks_cc import WebhooksCC, match_body_path
9
+
10
+ with WebhooksCC() as client: # reads WHK_API_KEY
11
+ endpoint = client.endpoints.create(name="checkout test")
12
+ request = client.requests.wait_for(
13
+ endpoint.slug, timeout="30s", match=match_body_path("type", "invoice.paid")
14
+ )
15
+
16
+ Every signature check for each provider is in :mod:`webhooks_cc.verify`.
17
+ """
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ from .client import Emails, Endpoints, Forwarding, Requests, RetryOptions, WebhooksCC
22
+ from .durations import Duration, parse_duration
23
+ from .email import (
24
+ DEFAULT_EMAIL_DOMAIN,
25
+ email_address,
26
+ extract_code,
27
+ extract_link,
28
+ is_valid_email_tag,
29
+ )
30
+ from .errors import (
31
+ NotFoundError,
32
+ RateLimitError,
33
+ TimeoutError,
34
+ UnauthorizedError,
35
+ WebhooksCCError,
36
+ WebhookVerificationError,
37
+ )
38
+ from .matchers import (
39
+ Matcher,
40
+ is_email_request,
41
+ match_all,
42
+ match_any,
43
+ match_body_path,
44
+ match_body_subset,
45
+ match_content_type,
46
+ match_email,
47
+ match_header,
48
+ match_method,
49
+ match_path,
50
+ match_query_param,
51
+ match_unverified,
52
+ match_verified,
53
+ )
54
+ from .models import (
55
+ BuiltRequest,
56
+ EmailAddress,
57
+ EmailAttachment,
58
+ EmailCapture,
59
+ EmailExtracts,
60
+ Endpoint,
61
+ ExtractedLink,
62
+ Request,
63
+ RequestPage,
64
+ SendTestEmailResult,
65
+ TeamShare,
66
+ UsageInfo,
67
+ )
68
+ from .templates import (
69
+ TEMPLATE_METADATA,
70
+ TEMPLATE_PROVIDERS,
71
+ VERIFY_PROVIDERS,
72
+ TemplateProviderInfo,
73
+ build_template_request,
74
+ )
75
+ from .testing import capture_during, capture_email_during, temporary_endpoint
76
+ from .verify import (
77
+ RelayedRequestInfo,
78
+ verify_forwarded_email,
79
+ verify_forwarded_request,
80
+ verify_relayed_request,
81
+ verify_signature,
82
+ )
83
+
84
+ __all__ = [
85
+ "DEFAULT_EMAIL_DOMAIN",
86
+ "TEMPLATE_METADATA",
87
+ "TEMPLATE_PROVIDERS",
88
+ "VERIFY_PROVIDERS",
89
+ "BuiltRequest",
90
+ "Duration",
91
+ "EmailAddress",
92
+ "EmailAttachment",
93
+ "EmailCapture",
94
+ "EmailExtracts",
95
+ "Emails",
96
+ "Endpoint",
97
+ "Endpoints",
98
+ "ExtractedLink",
99
+ "Forwarding",
100
+ "Matcher",
101
+ "NotFoundError",
102
+ "RateLimitError",
103
+ "RelayedRequestInfo",
104
+ "Request",
105
+ "RequestPage",
106
+ "Requests",
107
+ "RetryOptions",
108
+ "SendTestEmailResult",
109
+ "TeamShare",
110
+ "TemplateProviderInfo",
111
+ "TimeoutError",
112
+ "UnauthorizedError",
113
+ "UsageInfo",
114
+ "WebhookVerificationError",
115
+ "WebhooksCC",
116
+ "WebhooksCCError",
117
+ "__version__",
118
+ "build_template_request",
119
+ "capture_during",
120
+ "capture_email_during",
121
+ "email_address",
122
+ "extract_code",
123
+ "extract_link",
124
+ "is_email_request",
125
+ "is_valid_email_tag",
126
+ "match_all",
127
+ "match_any",
128
+ "match_body_path",
129
+ "match_body_subset",
130
+ "match_content_type",
131
+ "match_email",
132
+ "match_header",
133
+ "match_method",
134
+ "match_path",
135
+ "match_query_param",
136
+ "match_unverified",
137
+ "match_verified",
138
+ "parse_duration",
139
+ "temporary_endpoint",
140
+ "verify_forwarded_email",
141
+ "verify_forwarded_request",
142
+ "verify_relayed_request",
143
+ "verify_signature",
144
+ ]