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.
- dex_connector_postmark-0.1.0/.gitignore +6 -0
- dex_connector_postmark-0.1.0/PKG-INFO +50 -0
- dex_connector_postmark-0.1.0/README.md +39 -0
- dex_connector_postmark-0.1.0/examples/outbound_lookup/__init__.py +3 -0
- dex_connector_postmark-0.1.0/examples/outbound_lookup/flow.py +81 -0
- dex_connector_postmark-0.1.0/examples/outbound_lookup/worker.py +51 -0
- dex_connector_postmark-0.1.0/pyproject.toml +37 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/__init__.py +50 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/client.py +375 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/connection.py +135 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/dex-connector.json +82 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/models.py +37 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/postmarkmock/__init__.py +88 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/py.typed +0 -0
- dex_connector_postmark-0.1.0/src/dex_connectors/postmark/steps.py +111 -0
- dex_connector_postmark-0.1.0/tests/test_manifest_contract.py +65 -0
- dex_connector_postmark-0.1.0/tests/test_operations.py +180 -0
- dex_connector_postmark-0.1.0/uv.lock +932 -0
|
@@ -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,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
|
+
]
|