dex-connector-postmark 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-postmark
3
+ Version: 0.1.0
4
+ Summary: Postmark 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
+ # Postmark connector for Python
13
+
14
+ Install with `pip install dex-connector-postmark==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 Queries `listOutboundMessages` and `getMessageDetails` 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
+ | `listOutboundMessages` | `new_list_outbound_messages_step` | `listed` | `providerRejected`, `invalidResponse`, `defect` |
24
+ | `getMessageDetails` | `new_get_message_details_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
25
+
26
+ `listOutboundMessages` sends `count` (zero means 50, at most 500) and `offset`;
27
+ `count + offset` must stay within Postmark's 10,000-message search window.
28
+ `getMessageDetails` selects `notFound` for HTTP 404 or Postmark error code 701.
29
+ 408, 429, 5xx, and transport failures retry; other 4xx responses select
30
+ `providerRejected`.
31
+
32
+ ## Authorization
33
+
34
+ Create a Dex Web connection named `postmark` as described in the
35
+ [connector README](../README.md#authorization). The Server API token travels
36
+ only in the `X-Postmark-Server-Token` header and is resolved for every attempt.
37
+
38
+ ## Example
39
+
40
+ [`examples/outbound_lookup`](examples/outbound_lookup/flow.py) checks the page
41
+ window, lists outbound messages with `new_list_outbound_messages_step`, and
42
+ records the page. Run the Worker with
43
+ `uv run python -m examples.outbound_lookup.worker` after `dexcli dev` has
44
+ loaded a project containing the `postmark` connection.
45
+
46
+ ## Tests
47
+
48
+ `postmarkmock.Mock` scripts both Queries 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
+ # Postmark connector for Python
2
+
3
+ Install with `pip install dex-connector-postmark==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 Queries `listOutboundMessages` and `getMessageDetails` 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
+ | `listOutboundMessages` | `new_list_outbound_messages_step` | `listed` | `providerRejected`, `invalidResponse`, `defect` |
13
+ | `getMessageDetails` | `new_get_message_details_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
14
+
15
+ `listOutboundMessages` sends `count` (zero means 50, at most 500) and `offset`;
16
+ `count + offset` must stay within Postmark's 10,000-message search window.
17
+ `getMessageDetails` selects `notFound` for HTTP 404 or Postmark error code 701.
18
+ 408, 429, 5xx, and transport failures retry; other 4xx responses select
19
+ `providerRejected`.
20
+
21
+ ## Authorization
22
+
23
+ Create a Dex Web connection named `postmark` as described in the
24
+ [connector README](../README.md#authorization). The Server API token travels
25
+ only in the `X-Postmark-Server-Token` header and is resolved for every attempt.
26
+
27
+ ## Example
28
+
29
+ [`examples/outbound_lookup`](examples/outbound_lookup/flow.py) checks the page
30
+ window, lists outbound messages with `new_list_outbound_messages_step`, and
31
+ records the page. Run the Worker with
32
+ `uv run python -m examples.outbound_lookup.worker` after `dexcli dev` has
33
+ loaded a project containing the `postmark` connection.
34
+
35
+ ## Tests
36
+
37
+ `postmarkmock.Mock` scripts both Queries 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
+ """Postmark outbound lookup example Flow."""
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Runnable outbound lookup Flow using the Postmark listOutboundMessages Query."""
4
+
5
+ import dex
6
+ from dex_connectors.sdk import QueryResult
7
+
8
+ from dex_connectors.postmark import (
9
+ Connection,
10
+ ListOutboundMessagesInput,
11
+ MessagePage,
12
+ new_list_outbound_messages_step,
13
+ )
14
+
15
+ FLOW_TYPE = "PostmarkOutboundLookup"
16
+ CONNECTION_NAME = "postmark"
17
+ _LIST_STEP_TYPE = "ListOutboundMessages"
18
+ _OUTCOME = dex.Attribute("postmark-outbound-lookup-outcome", MessagePage)
19
+
20
+
21
+ def map_to_list_outbound_messages_input(request: ListOutboundMessagesInput) -> ListOutboundMessagesInput:
22
+ """Search with the requested page window unchanged."""
23
+ return request
24
+
25
+
26
+ # dex:group group-id:outbound-lookup group-label:"Outbound lookup"
27
+ # dex:explanation text:"Check the requested page window before calling Postmark."
28
+ class _PrepareRequest(dex.Step[ListOutboundMessagesInput]):
29
+ def get_step_type(self) -> str:
30
+ return "PrepareRequest"
31
+
32
+ def execute(self, context: dex.Context, input: ListOutboundMessagesInput) -> dex.StepDecision:
33
+ if input.count < 0 or input.offset < 0:
34
+ return dex.force_fail("count and offset must not be negative")
35
+ return dex.go_to(dex.StepRef(_LIST_STEP_TYPE), input)
36
+
37
+
38
+ # dex:group group-id:outbound-lookup group-label:"Outbound lookup"
39
+ # dex:explanation text:"Record the outbound message page and complete the Flow."
40
+ class _CompleteRequest(dex.Step[QueryResult[MessagePage]]):
41
+ def get_step_type(self) -> str:
42
+ return "CompleteRequest"
43
+
44
+ def execute(self, context: dex.Context, input: QueryResult[MessagePage]) -> dex.StepDecision:
45
+ _OUTCOME.set(context, input.value)
46
+ return dex.graceful_complete(input.value)
47
+
48
+
49
+ class OutboundLookupFlow(dex.Flow[ListOutboundMessagesInput]):
50
+ """List one page of outbound messages through the static postmark connection."""
51
+
52
+ def __init__(self, connection: Connection) -> None:
53
+ if connection.reference.name != CONNECTION_NAME:
54
+ raise ValueError("outbound lookup requires the static postmark connection")
55
+ self._prepare = _PrepareRequest()
56
+ self._complete = _CompleteRequest()
57
+ self._list = new_list_outbound_messages_step(
58
+ step_type=_LIST_STEP_TYPE,
59
+ step_input_type=ListOutboundMessagesInput,
60
+ annotations={
61
+ "groupId": "postmark",
62
+ "groupLabel": "Postmark",
63
+ "explanation": "List recent outbound messages from Postmark.",
64
+ },
65
+ connection=connection,
66
+ connection_name=CONNECTION_NAME,
67
+ map_to_operation_input=map_to_list_outbound_messages_input,
68
+ listed=self._complete,
69
+ )
70
+
71
+ def get_flow_type(self) -> str:
72
+ """Return the stable Flow identity used in Dex Web."""
73
+ return FLOW_TYPE
74
+
75
+ def get_steps(self) -> dex.StepList[ListOutboundMessagesInput]:
76
+ """Register request check, provider search, and completion Steps."""
77
+ return dex.StepList.start_step(self._prepare).other_steps(self._list, self._complete)
78
+
79
+ def get_persistence_schema(self) -> dex.PersistenceSchema:
80
+ """Persist the outbound message page."""
81
+ 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 Postmark outbound lookup 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.postmark import new_project_connection
14
+
15
+ from .flow import CONNECTION_NAME, OutboundLookupFlow
16
+
17
+
18
+ async def run_worker() -> None:
19
+ """Load the named Dex Web connection and serve outbound lookups."""
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([OutboundLookupFlow(connection)], allow_async_handlers=True)
26
+ cache_directory = os.environ.get(
27
+ "DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-postmark-outbound-lookup-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-postmark"
7
+ version = "0.1.0"
8
+ description = "Postmark 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
+ """Postmark outbound message operations for Dex connector Steps."""
4
+
5
+ from .client import (
6
+ CONNECTOR_ID,
7
+ DEFECT,
8
+ FOUND,
9
+ GET_MESSAGE_DETAILS_DEFINITION,
10
+ INVALID_RESPONSE,
11
+ LIST_OUTBOUND_MESSAGES_DEFINITION,
12
+ LISTED,
13
+ NOT_FOUND,
14
+ PROVIDER_REJECTED,
15
+ Client,
16
+ Config,
17
+ Credentials,
18
+ GetMessageDetailsOperation,
19
+ ListOutboundMessagesOperation,
20
+ )
21
+ from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
22
+ from .models import GetMessageDetailsInput, ListOutboundMessagesInput, MessageDetails, MessagePage
23
+ from .steps import new_get_message_details_step, new_list_outbound_messages_step
24
+
25
+ __all__ = [
26
+ "Client",
27
+ "Config",
28
+ "Connection",
29
+ "Credentials",
30
+ "GetMessageDetailsInput",
31
+ "GetMessageDetailsOperation",
32
+ "ListOutboundMessagesInput",
33
+ "ListOutboundMessagesOperation",
34
+ "MessageDetails",
35
+ "MessagePage",
36
+ "new_connection",
37
+ "new_connection_with_operations",
38
+ "new_get_message_details_step",
39
+ "new_list_outbound_messages_step",
40
+ "new_project_connection",
41
+ "CONNECTOR_ID",
42
+ "DEFECT",
43
+ "FOUND",
44
+ "GET_MESSAGE_DETAILS_DEFINITION",
45
+ "INVALID_RESPONSE",
46
+ "LIST_OUTBOUND_MESSAGES_DEFINITION",
47
+ "LISTED",
48
+ "NOT_FOUND",
49
+ "PROVIDER_REJECTED",
50
+ ]