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.
@@ -0,0 +1,6 @@
1
+ /.dex-local/dexcli
2
+ /.dex-local/dex-server-zd
3
+ /.dex-local/data/
4
+ /.dex-local/cli.log
5
+ /.dex-local/visualize.json
6
+ /.dex-local/visualize.err
@@ -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,3 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Resend welcome email example Flow."""
@@ -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
+ ]