dex-connector-resend 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.
- dex_connector_resend-0.1.0/.gitignore +6 -0
- dex_connector_resend-0.1.0/PKG-INFO +50 -0
- dex_connector_resend-0.1.0/README.md +39 -0
- dex_connector_resend-0.1.0/examples/welcome_email/__init__.py +3 -0
- dex_connector_resend-0.1.0/examples/welcome_email/flow.py +88 -0
- dex_connector_resend-0.1.0/examples/welcome_email/worker.py +51 -0
- dex_connector_resend-0.1.0/pyproject.toml +37 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/__init__.py +50 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/client.py +423 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/connection.py +124 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/dex-connector.json +82 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/models.py +40 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/py.typed +0 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/resendmock/__init__.py +90 -0
- dex_connector_resend-0.1.0/src/dex_connectors/resend/steps.py +118 -0
- dex_connector_resend-0.1.0/tests/test_manifest_contract.py +67 -0
- dex_connector_resend-0.1.0/tests/test_operations.py +219 -0
- dex_connector_resend-0.1.0/tests/test_welcome_email_flow.py +21 -0
- dex_connector_resend-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-resend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Resend connector for Dex Flows
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: dex-connectors-sdk==0.1.0
|
|
8
|
+
Requires-Dist: dex-python-sdk>=1.28.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Resend connector for Python
|
|
13
|
+
|
|
14
|
+
Install with `pip install dex-connector-resend==0.1.0`. The package requires
|
|
15
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
16
|
+
manifest operations `sendEmail` (Mutation) and `getEmail` (Query) with the same
|
|
17
|
+
branches, defaults, and failure classification as the Go module.
|
|
18
|
+
|
|
19
|
+
## Operations
|
|
20
|
+
|
|
21
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `sendEmail` | `new_send_email_step` | `sent` | `providerRejected`, `invalidResponse`, `defect` |
|
|
24
|
+
| `getEmail` | `new_get_email_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
25
|
+
|
|
26
|
+
`sendEmail` sends the stable Dex call ID in `Idempotency-Key`. Resend keeps
|
|
27
|
+
keys for 24 hours; the default one-hour retry budget stays within that window,
|
|
28
|
+
so keep any per-Step retry override shorter than 24 hours. A 409
|
|
29
|
+
`concurrent_idempotent_requests` retries with the same key, while
|
|
30
|
+
`invalid_idempotent_request` selects `providerRejected`. 408, 429, 5xx, and
|
|
31
|
+
transport failures retry; other 4xx responses select `providerRejected`.
|
|
32
|
+
|
|
33
|
+
## Authorization
|
|
34
|
+
|
|
35
|
+
Create a Dex Web connection named `resend` as described in the
|
|
36
|
+
[connector README](../README.md#authorization). The API key travels only in the
|
|
37
|
+
`Authorization` header and is resolved anew for every attempt.
|
|
38
|
+
|
|
39
|
+
## Example
|
|
40
|
+
|
|
41
|
+
[`examples/welcome_email`](examples/welcome_email/flow.py) checks the request,
|
|
42
|
+
sends it with `new_send_email_step`, and records the Resend email ID. Run the
|
|
43
|
+
Worker with `uv run python -m examples.welcome_email.worker` after
|
|
44
|
+
`dexcli dev` has loaded a project containing the `resend` connection.
|
|
45
|
+
|
|
46
|
+
## Tests
|
|
47
|
+
|
|
48
|
+
`resendmock.Mock` scripts both operations and defaults to the manifest mock
|
|
49
|
+
answers. Run `uv run pytest tests` for provider parity and manifest contract
|
|
50
|
+
checks.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Resend connector for Python
|
|
2
|
+
|
|
3
|
+
Install with `pip install dex-connector-resend==0.1.0`. The package requires
|
|
4
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
5
|
+
manifest operations `sendEmail` (Mutation) and `getEmail` (Query) with the same
|
|
6
|
+
branches, defaults, and failure classification as the Go module.
|
|
7
|
+
|
|
8
|
+
## Operations
|
|
9
|
+
|
|
10
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `sendEmail` | `new_send_email_step` | `sent` | `providerRejected`, `invalidResponse`, `defect` |
|
|
13
|
+
| `getEmail` | `new_get_email_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
14
|
+
|
|
15
|
+
`sendEmail` sends the stable Dex call ID in `Idempotency-Key`. Resend keeps
|
|
16
|
+
keys for 24 hours; the default one-hour retry budget stays within that window,
|
|
17
|
+
so keep any per-Step retry override shorter than 24 hours. A 409
|
|
18
|
+
`concurrent_idempotent_requests` retries with the same key, while
|
|
19
|
+
`invalid_idempotent_request` selects `providerRejected`. 408, 429, 5xx, and
|
|
20
|
+
transport failures retry; other 4xx responses select `providerRejected`.
|
|
21
|
+
|
|
22
|
+
## Authorization
|
|
23
|
+
|
|
24
|
+
Create a Dex Web connection named `resend` as described in the
|
|
25
|
+
[connector README](../README.md#authorization). The API key travels only in the
|
|
26
|
+
`Authorization` header and is resolved anew for every attempt.
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
[`examples/welcome_email`](examples/welcome_email/flow.py) checks the request,
|
|
31
|
+
sends it with `new_send_email_step`, and records the Resend email ID. Run the
|
|
32
|
+
Worker with `uv run python -m examples.welcome_email.worker` after
|
|
33
|
+
`dexcli dev` has loaded a project containing the `resend` connection.
|
|
34
|
+
|
|
35
|
+
## Tests
|
|
36
|
+
|
|
37
|
+
`resendmock.Mock` scripts both operations and defaults to the manifest mock
|
|
38
|
+
answers. Run `uv run pytest tests` for provider parity and manifest contract
|
|
39
|
+
checks.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Runnable welcome email Flow using the Resend sendEmail Mutation."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import dex
|
|
8
|
+
from dex_connectors.sdk import MutationResult
|
|
9
|
+
|
|
10
|
+
from dex_connectors.resend import Connection, SendEmailInput, SentEmail, new_send_email_step
|
|
11
|
+
|
|
12
|
+
FLOW_TYPE = "ResendWelcomeEmail"
|
|
13
|
+
CONNECTION_NAME = "resend"
|
|
14
|
+
_SEND_STEP_TYPE = "SendEmail"
|
|
15
|
+
_OUTCOME = dex.Attribute("resend-welcome-email-outcome", SentEmail)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class WelcomeEmailRequest:
|
|
20
|
+
"""Sender on a Resend-verified domain, recipients, subject, and HTML body."""
|
|
21
|
+
|
|
22
|
+
from_address: str = field(metadata={"dex_json_name": "from"})
|
|
23
|
+
to: tuple[str, ...]
|
|
24
|
+
subject: str
|
|
25
|
+
html: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def map_to_send_email_input(request: WelcomeEmailRequest) -> SendEmailInput:
|
|
29
|
+
"""Send the request unchanged; Resend deduplicates retries by Call ID."""
|
|
30
|
+
return SendEmailInput(request.from_address, request.to, request.subject, request.html)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# dex:group group-id:welcome-email group-label:"Welcome email"
|
|
34
|
+
# dex:explanation text:"Check the welcome email fields before calling Resend."
|
|
35
|
+
class _PrepareRequest(dex.Step[WelcomeEmailRequest]):
|
|
36
|
+
def get_step_type(self) -> str:
|
|
37
|
+
return "PrepareRequest"
|
|
38
|
+
|
|
39
|
+
def execute(self, context: dex.Context, input: WelcomeEmailRequest) -> dex.StepDecision:
|
|
40
|
+
if not input.from_address.strip() or not input.to or not input.subject.strip() or not input.html:
|
|
41
|
+
return dex.force_fail("from, to, subject, and html are required")
|
|
42
|
+
return dex.go_to(dex.StepRef(_SEND_STEP_TYPE), input)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# dex:group group-id:welcome-email group-label:"Welcome email"
|
|
46
|
+
# dex:explanation text:"Record the Resend email ID and complete the Flow."
|
|
47
|
+
class _CompleteRequest(dex.Step[MutationResult[SentEmail]]):
|
|
48
|
+
def get_step_type(self) -> str:
|
|
49
|
+
return "CompleteRequest"
|
|
50
|
+
|
|
51
|
+
def execute(self, context: dex.Context, input: MutationResult[SentEmail]) -> dex.StepDecision:
|
|
52
|
+
_OUTCOME.set(context, input.value)
|
|
53
|
+
return dex.graceful_complete(input.value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WelcomeEmailFlow(dex.Flow[WelcomeEmailRequest]):
|
|
57
|
+
"""Send one welcome email through the static resend connection."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, connection: Connection) -> None:
|
|
60
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
61
|
+
raise ValueError("welcome email requires the static resend connection")
|
|
62
|
+
self._prepare = _PrepareRequest()
|
|
63
|
+
self._complete = _CompleteRequest()
|
|
64
|
+
self._send = new_send_email_step(
|
|
65
|
+
step_type=_SEND_STEP_TYPE,
|
|
66
|
+
step_input_type=WelcomeEmailRequest,
|
|
67
|
+
annotations={
|
|
68
|
+
"groupId": "resend",
|
|
69
|
+
"groupLabel": "Resend",
|
|
70
|
+
"explanation": "Send the welcome email through Resend.",
|
|
71
|
+
},
|
|
72
|
+
connection=connection,
|
|
73
|
+
connection_name=CONNECTION_NAME,
|
|
74
|
+
map_to_operation_input=map_to_send_email_input,
|
|
75
|
+
sent=self._complete,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_flow_type(self) -> str:
|
|
79
|
+
"""Return the stable Flow identity used in Dex Web."""
|
|
80
|
+
return FLOW_TYPE
|
|
81
|
+
|
|
82
|
+
def get_steps(self) -> dex.StepList[WelcomeEmailRequest]:
|
|
83
|
+
"""Register request check, provider send, and completion Steps."""
|
|
84
|
+
return dex.StepList.start_step(self._prepare).other_steps(self._send, self._complete)
|
|
85
|
+
|
|
86
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
87
|
+
"""Persist the Resend email identifier."""
|
|
88
|
+
return dex.PersistenceSchema.of(_OUTCOME)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Worker entrypoint for the Resend welcome email Flow."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import dex
|
|
11
|
+
from dex_connectors.sdk.projectconfig import load_from_environment
|
|
12
|
+
|
|
13
|
+
from dex_connectors.resend import new_project_connection
|
|
14
|
+
|
|
15
|
+
from .flow import CONNECTION_NAME, WelcomeEmailFlow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_worker() -> None:
|
|
19
|
+
"""Load the named Dex Web connection and send welcome emails."""
|
|
20
|
+
project = await load_from_environment()
|
|
21
|
+
connection = None
|
|
22
|
+
cache = None
|
|
23
|
+
try:
|
|
24
|
+
connection = new_project_connection(project, CONNECTION_NAME)
|
|
25
|
+
registry = dex.Registry([WelcomeEmailFlow(connection)], allow_async_handlers=True)
|
|
26
|
+
cache_directory = os.environ.get(
|
|
27
|
+
"DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-resend-welcome-email-blobs")
|
|
28
|
+
)
|
|
29
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(cache_directory, 1 << 30))
|
|
30
|
+
worker = dex.AsyncWorker(
|
|
31
|
+
registry,
|
|
32
|
+
cache,
|
|
33
|
+
dex.WorkerOptions(server_address=os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")),
|
|
34
|
+
)
|
|
35
|
+
async with worker:
|
|
36
|
+
await worker.start()
|
|
37
|
+
finally:
|
|
38
|
+
if connection is not None:
|
|
39
|
+
await connection.aclose()
|
|
40
|
+
if cache is not None:
|
|
41
|
+
cache.close()
|
|
42
|
+
await project.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
"""Run until the Worker stops or the process receives an interrupt."""
|
|
47
|
+
asyncio.run(run_worker())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dex-connector-resend"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Resend connector for Dex Flows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["dex-connectors-sdk==0.1.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "pyyaml>=6", "types-PyYAML>=6"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/dex_connectors"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
pythonpath = [".", "src"]
|
|
23
|
+
asyncio_mode = "auto"
|
|
24
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
python_version = "3.11"
|
|
28
|
+
strict = true
|
|
29
|
+
mypy_path = "src"
|
|
30
|
+
explicit_package_bases = true
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
line-length = 110
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Resend email operations for Dex connector Steps."""
|
|
4
|
+
|
|
5
|
+
from .client import (
|
|
6
|
+
CONNECTOR_ID,
|
|
7
|
+
DEFECT,
|
|
8
|
+
FOUND,
|
|
9
|
+
GET_EMAIL_DEFINITION,
|
|
10
|
+
INVALID_RESPONSE,
|
|
11
|
+
NOT_FOUND,
|
|
12
|
+
PROVIDER_REJECTED,
|
|
13
|
+
SEND_EMAIL_DEFINITION,
|
|
14
|
+
SENT,
|
|
15
|
+
Client,
|
|
16
|
+
Config,
|
|
17
|
+
Credentials,
|
|
18
|
+
GetEmailOperation,
|
|
19
|
+
SendEmailOperation,
|
|
20
|
+
)
|
|
21
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
22
|
+
from .models import Email, GetEmailInput, SendEmailInput, SentEmail
|
|
23
|
+
from .steps import new_get_email_step, new_send_email_step
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Client",
|
|
27
|
+
"Config",
|
|
28
|
+
"Connection",
|
|
29
|
+
"Credentials",
|
|
30
|
+
"Email",
|
|
31
|
+
"GetEmailInput",
|
|
32
|
+
"GetEmailOperation",
|
|
33
|
+
"SendEmailInput",
|
|
34
|
+
"SendEmailOperation",
|
|
35
|
+
"SentEmail",
|
|
36
|
+
"new_connection",
|
|
37
|
+
"new_connection_with_operations",
|
|
38
|
+
"new_get_email_step",
|
|
39
|
+
"new_project_connection",
|
|
40
|
+
"new_send_email_step",
|
|
41
|
+
"CONNECTOR_ID",
|
|
42
|
+
"DEFECT",
|
|
43
|
+
"FOUND",
|
|
44
|
+
"GET_EMAIL_DEFINITION",
|
|
45
|
+
"INVALID_RESPONSE",
|
|
46
|
+
"NOT_FOUND",
|
|
47
|
+
"PROVIDER_REJECTED",
|
|
48
|
+
"SEND_EMAIL_DEFINITION",
|
|
49
|
+
"SENT",
|
|
50
|
+
]
|