dex-connector-front 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_front-0.1.0/.gitignore +16 -0
- dex_connector_front-0.1.0/LICENSE +21 -0
- dex_connector_front-0.1.0/PKG-INFO +78 -0
- dex_connector_front-0.1.0/README.md +65 -0
- dex_connector_front-0.1.0/examples/__init__.py +3 -0
- dex_connector_front-0.1.0/examples/contact_lookup/README.md +29 -0
- dex_connector_front-0.1.0/examples/contact_lookup/__init__.py +3 -0
- dex_connector_front-0.1.0/examples/contact_lookup/flow.py +78 -0
- dex_connector_front-0.1.0/examples/contact_lookup/integration_test.py +89 -0
- dex_connector_front-0.1.0/examples/contact_lookup/main.py +27 -0
- dex_connector_front-0.1.0/examples/contact_lookup/sample-input.json +1 -0
- dex_connector_front-0.1.0/examples/contact_lookup/worker.py +26 -0
- dex_connector_front-0.1.0/pyproject.toml +41 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/__init__.py +62 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/__init__.py +2 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/config.py +23 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/connection.py +151 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/definitions.py +113 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/mock.py +338 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/models.py +424 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/steps.py +296 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/_generated/timestamps.py +60 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/client.py +467 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/connection.py +5 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/dex-connector.json +201 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/frontmock/__init__.py +6 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/models.py +5 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/operations.py +958 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/py.typed +0 -0
- dex_connector_front-0.1.0/src/dex_connectors/front/steps.py +5 -0
- dex_connector_front-0.1.0/tests/test_client.py +106 -0
- dex_connector_front-0.1.0/tests/test_conformance.py +38 -0
- dex_connector_front-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
node_modules/
|
|
5
|
+
dist/
|
|
6
|
+
sdkpython/dist/dex_connectors_sdk-0.1.0-py3-none-any.whl
|
|
7
|
+
/.bin/dexcli
|
|
8
|
+
/.bin/dex-server-zd
|
|
9
|
+
/sdkpython/.dexcli-data/
|
|
10
|
+
site/.catalog.yaml
|
|
11
|
+
coverage/
|
|
12
|
+
*.out
|
|
13
|
+
*.test
|
|
14
|
+
go.work
|
|
15
|
+
go.work.sum
|
|
16
|
+
.idea/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Super Durable
|
|
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,78 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-front
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dex-native Front conversation and contact connector
|
|
5
|
+
Project-URL: Source, https://github.com/superdurable/dex-connectors-library/tree/main/connectors/front/python
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: dex-connectors-sdk==0.1.0
|
|
10
|
+
Requires-Dist: dex-python-sdk>=1.28.0
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Front connector for Python
|
|
15
|
+
|
|
16
|
+
`dex-connector-front==0.1.0` implements all five Front operations as Dex Python
|
|
17
|
+
Queries and Mutations: `searchConversations`, `getConversation`,
|
|
18
|
+
`replyToConversation`, `updateConversation`, and `findContactByEmail`. It has
|
|
19
|
+
no Python Trigger. Import from `dex_connectors.front`; the generated models,
|
|
20
|
+
Step factories, connection binding, mock outcomes, and `dex-connector.json`
|
|
21
|
+
come from [`connector.yaml`](../connector.yaml).
|
|
22
|
+
|
|
23
|
+
The connector requires Python 3.11 or newer, Dex Python SDK 1.28.0 or newer,
|
|
24
|
+
and the pinned `dex-connectors-sdk` release in `pyproject.toml`.
|
|
25
|
+
|
|
26
|
+
## Connection
|
|
27
|
+
|
|
28
|
+
The runnable example uses the static name `front-support`. Create an API token
|
|
29
|
+
at <https://app.frontapp.com/> under **Settings > Developers > API Tokens**.
|
|
30
|
+
A company admin chooses **Create API token**, grants contact read access for
|
|
31
|
+
this example, then copies the token into the secret `api_token` field in Dex Web
|
|
32
|
+
**Connections**. The token has three dot-separated JWT segments and is sent
|
|
33
|
+
only as an Authorization bearer token. The `maxResponseBytes` setting limits
|
|
34
|
+
one complete response in bytes; leave it blank to retain the displayed safety
|
|
35
|
+
limit, and raise it only when a valid large response selects `invalidResponse`.
|
|
36
|
+
The [Front connector guide](../README.md#authorization) details permissions
|
|
37
|
+
for every operation and the inbox, teammate, and tag pickers.
|
|
38
|
+
|
|
39
|
+
The runnable [`main.py`](examples/contact_lookup/main.py) loads project
|
|
40
|
+
configuration and resolves private credentials at call time through
|
|
41
|
+
`new_project_connection(project, "front-support")`. Secrets stay out of Flow
|
|
42
|
+
input, Results, Attributes, receipts, logs, and generated manifests.
|
|
43
|
+
|
|
44
|
+
## Operation outcomes
|
|
45
|
+
|
|
46
|
+
The generated operation-specific factories accept one required happy-path
|
|
47
|
+
branch. Optional branches fail the Flow when left unwired. Searches select
|
|
48
|
+
`searched`; conversation reads and contact lookups select `found`; replies
|
|
49
|
+
select `replied`; updates select `updated`. Reads and updates retry safe
|
|
50
|
+
transient failures. A reply or internal comment uses the SDK single-dispatch
|
|
51
|
+
fence because Front has no provider idempotency key. Ambiguous delivery
|
|
52
|
+
selects `uncertain`; reconcile it in Front before making a new request. An
|
|
53
|
+
update reads back its requested values and can repeat absolute writes.
|
|
54
|
+
|
|
55
|
+
`frontmock.Mock` and its generated named scenarios can exercise branch routing
|
|
56
|
+
without credentials. The 50 Go-recorded provider scenarios in
|
|
57
|
+
[`conformance/connectors/front`](../../../conformance/connectors/front/scenarios.json)
|
|
58
|
+
exercise exact HTTP requests, response decoding, branches, retry decisions,
|
|
59
|
+
and the production SDK output codec. They supplement the real Dex Worker and
|
|
60
|
+
Client [example integration test](examples/contact_lookup/integration_test.py).
|
|
61
|
+
|
|
62
|
+
## Run the example
|
|
63
|
+
|
|
64
|
+
See the [contact lookup Flow](examples/contact_lookup/README.md). From this
|
|
65
|
+
package directory, install and run its Worker after configuring Dex Web:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
uv sync --locked
|
|
69
|
+
DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:8801 \
|
|
70
|
+
DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
|
|
71
|
+
uv run python -m examples.contact_lookup.main
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For local verification with `dexcli dev`, use `uv run pytest` for the package
|
|
75
|
+
suite and set `DEX_FLOW_SERVICE_ADDRESS` to the active local Dex gRPC address
|
|
76
|
+
when running `uv run pytest examples/contact_lookup/integration_test.py`.
|
|
77
|
+
The local integration test uses a free-port Front stand-in; Front account
|
|
78
|
+
setup, API permissions, and live provider behavior remain unverified.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Front connector for Python
|
|
2
|
+
|
|
3
|
+
`dex-connector-front==0.1.0` implements all five Front operations as Dex Python
|
|
4
|
+
Queries and Mutations: `searchConversations`, `getConversation`,
|
|
5
|
+
`replyToConversation`, `updateConversation`, and `findContactByEmail`. It has
|
|
6
|
+
no Python Trigger. Import from `dex_connectors.front`; the generated models,
|
|
7
|
+
Step factories, connection binding, mock outcomes, and `dex-connector.json`
|
|
8
|
+
come from [`connector.yaml`](../connector.yaml).
|
|
9
|
+
|
|
10
|
+
The connector requires Python 3.11 or newer, Dex Python SDK 1.28.0 or newer,
|
|
11
|
+
and the pinned `dex-connectors-sdk` release in `pyproject.toml`.
|
|
12
|
+
|
|
13
|
+
## Connection
|
|
14
|
+
|
|
15
|
+
The runnable example uses the static name `front-support`. Create an API token
|
|
16
|
+
at <https://app.frontapp.com/> under **Settings > Developers > API Tokens**.
|
|
17
|
+
A company admin chooses **Create API token**, grants contact read access for
|
|
18
|
+
this example, then copies the token into the secret `api_token` field in Dex Web
|
|
19
|
+
**Connections**. The token has three dot-separated JWT segments and is sent
|
|
20
|
+
only as an Authorization bearer token. The `maxResponseBytes` setting limits
|
|
21
|
+
one complete response in bytes; leave it blank to retain the displayed safety
|
|
22
|
+
limit, and raise it only when a valid large response selects `invalidResponse`.
|
|
23
|
+
The [Front connector guide](../README.md#authorization) details permissions
|
|
24
|
+
for every operation and the inbox, teammate, and tag pickers.
|
|
25
|
+
|
|
26
|
+
The runnable [`main.py`](examples/contact_lookup/main.py) loads project
|
|
27
|
+
configuration and resolves private credentials at call time through
|
|
28
|
+
`new_project_connection(project, "front-support")`. Secrets stay out of Flow
|
|
29
|
+
input, Results, Attributes, receipts, logs, and generated manifests.
|
|
30
|
+
|
|
31
|
+
## Operation outcomes
|
|
32
|
+
|
|
33
|
+
The generated operation-specific factories accept one required happy-path
|
|
34
|
+
branch. Optional branches fail the Flow when left unwired. Searches select
|
|
35
|
+
`searched`; conversation reads and contact lookups select `found`; replies
|
|
36
|
+
select `replied`; updates select `updated`. Reads and updates retry safe
|
|
37
|
+
transient failures. A reply or internal comment uses the SDK single-dispatch
|
|
38
|
+
fence because Front has no provider idempotency key. Ambiguous delivery
|
|
39
|
+
selects `uncertain`; reconcile it in Front before making a new request. An
|
|
40
|
+
update reads back its requested values and can repeat absolute writes.
|
|
41
|
+
|
|
42
|
+
`frontmock.Mock` and its generated named scenarios can exercise branch routing
|
|
43
|
+
without credentials. The 50 Go-recorded provider scenarios in
|
|
44
|
+
[`conformance/connectors/front`](../../../conformance/connectors/front/scenarios.json)
|
|
45
|
+
exercise exact HTTP requests, response decoding, branches, retry decisions,
|
|
46
|
+
and the production SDK output codec. They supplement the real Dex Worker and
|
|
47
|
+
Client [example integration test](examples/contact_lookup/integration_test.py).
|
|
48
|
+
|
|
49
|
+
## Run the example
|
|
50
|
+
|
|
51
|
+
See the [contact lookup Flow](examples/contact_lookup/README.md). From this
|
|
52
|
+
package directory, install and run its Worker after configuring Dex Web:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uv sync --locked
|
|
56
|
+
DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:8801 \
|
|
57
|
+
DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
|
|
58
|
+
uv run python -m examples.contact_lookup.main
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For local verification with `dexcli dev`, use `uv run pytest` for the package
|
|
62
|
+
suite and set `DEX_FLOW_SERVICE_ADDRESS` to the active local Dex gRPC address
|
|
63
|
+
when running `uv run pytest examples/contact_lookup/integration_test.py`.
|
|
64
|
+
The local integration test uses a free-port Front stand-in; Front account
|
|
65
|
+
setup, API permissions, and live provider behavior remain unverified.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Front contact lookup Flow
|
|
2
|
+
|
|
3
|
+
This Flow uses the static `front-support` connection to find the Front contact
|
|
4
|
+
that owns one email handle. Run its Worker with `python -m
|
|
5
|
+
examples.contact_lookup.main` after configuring the connection through Dex Web.
|
|
6
|
+
The connection holds an API token; the Flow input is only an email address.
|
|
7
|
+
|
|
8
|
+
Start `dexcli dev` and configure `front / front-support` in Dex Web **Connections**.
|
|
9
|
+
Create the Front API token using the authorization guide in
|
|
10
|
+
[`connector.yaml`](../../../connector.yaml). In Front, an admin opens
|
|
11
|
+
<https://app.frontapp.com/> > **Settings > Developers > API Tokens**,
|
|
12
|
+
creates a token with contact read access, and copies its secret into Dex Web.
|
|
13
|
+
Leave the response byte limit blank for the displayed default. Set
|
|
14
|
+
`DEX_CONNECTOR_CONFIG_FILE` to the private `connections.json` exported by that
|
|
15
|
+
project, then run:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
uv sync --locked
|
|
19
|
+
DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:8801 \
|
|
20
|
+
DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
|
|
21
|
+
uv run python -m examples.contact_lookup.main
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The Worker registration uses the real application source in [`main.py`](main.py).
|
|
25
|
+
Start `FrontContactLookupPython` with `sample-input.json` using a Dex Client.
|
|
26
|
+
Only the `found` branch is wired; a missing contact, provider rejection,
|
|
27
|
+
invalid response, or local defect fails the Flow. `integration_test.py` runs
|
|
28
|
+
this Flow through a real Dex Worker and Client against a local Front stand-in.
|
|
29
|
+
Live Front authorization and provider behavior remain unverified.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Find a Front contact through one durable Dex Query Step."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import dex
|
|
8
|
+
from dex_connectors.sdk import QueryResult
|
|
9
|
+
|
|
10
|
+
from dex_connectors.front import (
|
|
11
|
+
Connection,
|
|
12
|
+
FindContactByEmailInput,
|
|
13
|
+
FindContactByEmailOutput,
|
|
14
|
+
new_find_contact_by_email_step,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CONNECTION_NAME = "front-support"
|
|
18
|
+
FLOW_TYPE = "FrontContactLookupPython"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ContactLookupRequest:
|
|
23
|
+
"""Customer email entered when starting the Flow."""
|
|
24
|
+
|
|
25
|
+
email: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def map_to_contact_lookup_input(request: ContactLookupRequest) -> FindContactByEmailInput:
|
|
29
|
+
"""Map the safe Flow input to Front's email alias lookup."""
|
|
30
|
+
return FindContactByEmailInput(email=request.email)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# dex:group group-id:front-contact group-label:"Front contact"
|
|
34
|
+
# dex:explanation text:"Return the Front contact found for the requested email."
|
|
35
|
+
class CompleteContactLookup(dex.Step[QueryResult[FindContactByEmailOutput]]):
|
|
36
|
+
"""Complete the Flow with the contact lookup output."""
|
|
37
|
+
|
|
38
|
+
def get_step_type(self) -> str:
|
|
39
|
+
"""Return the stable completion Step identity."""
|
|
40
|
+
return "CompleteFrontContactLookup"
|
|
41
|
+
|
|
42
|
+
def execute(self, context: dex.Context, input: QueryResult[FindContactByEmailOutput]) -> dex.StepDecision:
|
|
43
|
+
"""Keep the result free of connection credentials."""
|
|
44
|
+
return dex.graceful_complete(input.value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ContactLookupFlow(dex.Flow[ContactLookupRequest]):
|
|
48
|
+
"""Find a contact through the static Front connection."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, connection: Connection) -> None:
|
|
51
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
52
|
+
raise ValueError("Front contact lookup requires its static connection name")
|
|
53
|
+
self.complete = CompleteContactLookup()
|
|
54
|
+
self.lookup = new_find_contact_by_email_step(
|
|
55
|
+
step_type="FindFrontContactByEmail",
|
|
56
|
+
step_input_type=ContactLookupRequest,
|
|
57
|
+
annotations={
|
|
58
|
+
"groupId": "front-contact",
|
|
59
|
+
"groupLabel": "Front contact",
|
|
60
|
+
"explanation": "Find the Front contact with this email handle.",
|
|
61
|
+
},
|
|
62
|
+
connection=connection,
|
|
63
|
+
connection_name="front-support",
|
|
64
|
+
map_to_operation_input=map_to_contact_lookup_input,
|
|
65
|
+
found=self.complete,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def get_flow_type(self) -> str:
|
|
69
|
+
"""Return the stable application Flow type."""
|
|
70
|
+
return FLOW_TYPE
|
|
71
|
+
|
|
72
|
+
def get_steps(self) -> dex.StepList[ContactLookupRequest]:
|
|
73
|
+
"""Register the provider Query and completion Step."""
|
|
74
|
+
return dex.StepList.start_step(self.lookup).other_steps(self.complete)
|
|
75
|
+
|
|
76
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
77
|
+
"""Persist no Attributes; completion carries the safe contact."""
|
|
78
|
+
return dex.PersistenceSchema.of()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Exercise the Front example through a real Dex Worker and Client."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from threading import Thread
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
import dex
|
|
14
|
+
import pytest
|
|
15
|
+
from dex_connectors.sdk import ConnectionRef, SecretString, StaticCredentialProvider
|
|
16
|
+
|
|
17
|
+
from dex_connectors.front import Client, Config, Credentials, FindContactByEmailOutput, new_connection
|
|
18
|
+
|
|
19
|
+
from .flow import CONNECTION_NAME, ContactLookupFlow, ContactLookupRequest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_contact_lookup_retries_and_fails_unwired_not_found(tmp_path: Path) -> None:
|
|
24
|
+
"""Verify durable retry and optional branch routing against a local provider."""
|
|
25
|
+
server_address = os.environ["DEX_FLOW_SERVICE_ADDRESS"]
|
|
26
|
+
requests: list[str] = []
|
|
27
|
+
|
|
28
|
+
class FrontHandler(BaseHTTPRequestHandler):
|
|
29
|
+
def do_GET(self) -> None:
|
|
30
|
+
requests.append(self.path)
|
|
31
|
+
if "missing" in self.path:
|
|
32
|
+
self.send_response(404)
|
|
33
|
+
self.end_headers()
|
|
34
|
+
return
|
|
35
|
+
if len(requests) == 1:
|
|
36
|
+
self.send_response(429)
|
|
37
|
+
self.end_headers()
|
|
38
|
+
return
|
|
39
|
+
body = (
|
|
40
|
+
b'{"id":"crd_100","name":"Jane","handles":'
|
|
41
|
+
b'[{"handle":"jane@acme.example.com","source":"email"}]}'
|
|
42
|
+
)
|
|
43
|
+
self.send_response(200)
|
|
44
|
+
self.send_header("Content-Type", "application/json")
|
|
45
|
+
self.send_header("Content-Length", str(len(body)))
|
|
46
|
+
self.end_headers()
|
|
47
|
+
self.wfile.write(body)
|
|
48
|
+
|
|
49
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
50
|
+
pass # The stand-in logs no request details or credentials.
|
|
51
|
+
|
|
52
|
+
provider = ThreadingHTTPServer(("127.0.0.1", 0), FrontHandler)
|
|
53
|
+
provider_thread = Thread(target=provider.serve_forever, daemon=True)
|
|
54
|
+
provider_thread.start()
|
|
55
|
+
reference = ConnectionRef("front", CONNECTION_NAME)
|
|
56
|
+
credentials = StaticCredentialProvider({reference: Credentials(SecretString("front-inert-test-token"))})
|
|
57
|
+
client = Client(Config(), credentials, api_base_url=f"http://127.0.0.1:{provider.server_port}")
|
|
58
|
+
connection = new_connection(client, reference)
|
|
59
|
+
flow = ContactLookupFlow(connection)
|
|
60
|
+
registry = dex.Registry([flow], allow_async_handlers=True)
|
|
61
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(str(tmp_path / "blobs"), 1 << 24))
|
|
62
|
+
worker = dex.AsyncWorker(registry, cache, dex.WorkerOptions(server_address=server_address))
|
|
63
|
+
worker_task = asyncio.create_task(worker.start())
|
|
64
|
+
try:
|
|
65
|
+
await worker.wait_pull_registered(timeout=30)
|
|
66
|
+
async with dex.AsyncClient(
|
|
67
|
+
registry, cache, dex.ClientOptions(server_address=server_address)
|
|
68
|
+
) as dex_client:
|
|
69
|
+
success_id = f"front-python-found-{uuid4().hex}"
|
|
70
|
+
await dex_client.start_flow(flow, success_id, ContactLookupRequest("jane@acme.example.com"))
|
|
71
|
+
success = await asyncio.wait_for(dex_client.wait_for_flow(success_id, timedelta(seconds=30)), 40)
|
|
72
|
+
assert success.status == dex.FlowStatus.COMPLETED, success.error_message
|
|
73
|
+
output = success.single_output(FindContactByEmailOutput)
|
|
74
|
+
assert output.contacts[0].id == "crd_100"
|
|
75
|
+
assert requests.count("/contacts/alt:email:jane@acme.example.com") == 2
|
|
76
|
+
|
|
77
|
+
missing_id = f"front-python-missing-{uuid4().hex}"
|
|
78
|
+
await dex_client.start_flow(flow, missing_id, ContactLookupRequest("missing@acme.example.com"))
|
|
79
|
+
missing = await asyncio.wait_for(dex_client.wait_for_flow(missing_id, timedelta(seconds=30)), 40)
|
|
80
|
+
assert missing.status == dex.FlowStatus.FAILED
|
|
81
|
+
assert missing.error_message is not None and "notFound" in missing.error_message
|
|
82
|
+
finally:
|
|
83
|
+
await worker.stop()
|
|
84
|
+
await worker_task
|
|
85
|
+
cache.close()
|
|
86
|
+
await connection.aclose()
|
|
87
|
+
provider.shutdown()
|
|
88
|
+
provider.server_close()
|
|
89
|
+
provider_thread.join(timeout=5)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Load the Front connection and run the contact lookup Worker."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk.projectconfig import load_from_environment
|
|
8
|
+
|
|
9
|
+
from dex_connectors.front import new_project_connection
|
|
10
|
+
|
|
11
|
+
from .flow import CONNECTION_NAME, ContactLookupFlow
|
|
12
|
+
from .worker import run_worker
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def main() -> None:
|
|
16
|
+
"""Bind current project credentials to the static Front connection."""
|
|
17
|
+
project = await load_from_environment()
|
|
18
|
+
connection = new_project_connection(project, CONNECTION_NAME)
|
|
19
|
+
try:
|
|
20
|
+
await run_worker(ContactLookupFlow(connection))
|
|
21
|
+
finally:
|
|
22
|
+
await connection.aclose()
|
|
23
|
+
await project.close()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
asyncio.run(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"email":"jane@acme.example.com"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Serve the Front contact lookup Flow with a Dex Worker."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import dex
|
|
10
|
+
|
|
11
|
+
from .flow import ContactLookupFlow
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def run_worker(flow: ContactLookupFlow) -> None:
|
|
15
|
+
"""Run the Flow until the Worker is stopped."""
|
|
16
|
+
registry = dex.Registry([flow], allow_async_handlers=True)
|
|
17
|
+
directory = Path(
|
|
18
|
+
os.environ.get("DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-front-python-blobs"))
|
|
19
|
+
)
|
|
20
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(str(directory), 1 << 30))
|
|
21
|
+
try:
|
|
22
|
+
address = os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")
|
|
23
|
+
async with dex.AsyncWorker(registry, cache, dex.WorkerOptions(server_address=address)) as worker:
|
|
24
|
+
await worker.start()
|
|
25
|
+
finally:
|
|
26
|
+
cache.close()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dex-connector-front"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Dex-native Front conversation and contact connector"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = ["dex-connectors-sdk==0.1.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Source = "https://github.com/superdurable/dex-connectors-library/tree/main/connectors/front/python"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "PyYAML>=6", "types-PyYAML>=6"]
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/dex_connectors"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
pythonpath = [".", "src"]
|
|
27
|
+
asyncio_mode = "auto"
|
|
28
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
29
|
+
|
|
30
|
+
[tool.mypy]
|
|
31
|
+
python_version = "3.11"
|
|
32
|
+
strict = true
|
|
33
|
+
mypy_path = "src"
|
|
34
|
+
explicit_package_bases = true
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
target-version = "py311"
|
|
38
|
+
line-length = 110
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Front conversation and contact operations for Dex Python Flows."""
|
|
4
|
+
|
|
5
|
+
from .client import Client, Config, Credentials
|
|
6
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
7
|
+
from .models import (
|
|
8
|
+
Comment,
|
|
9
|
+
Contact,
|
|
10
|
+
ContactHandle,
|
|
11
|
+
Conversation,
|
|
12
|
+
ConversationDetails,
|
|
13
|
+
ConversationReply,
|
|
14
|
+
FindContactByEmailInput,
|
|
15
|
+
FindContactByEmailOutput,
|
|
16
|
+
GetConversationInput,
|
|
17
|
+
Message,
|
|
18
|
+
ReplyToConversationInput,
|
|
19
|
+
SearchConversationsInput,
|
|
20
|
+
SearchConversationsOutput,
|
|
21
|
+
UpdateConversationInput,
|
|
22
|
+
UpdateConversationOutput,
|
|
23
|
+
)
|
|
24
|
+
from .operations import build_reply_html
|
|
25
|
+
from .steps import (
|
|
26
|
+
new_find_contact_by_email_step,
|
|
27
|
+
new_get_conversation_step,
|
|
28
|
+
new_reply_to_conversation_step,
|
|
29
|
+
new_search_conversations_step,
|
|
30
|
+
new_update_conversation_step,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Client",
|
|
35
|
+
"Config",
|
|
36
|
+
"Credentials",
|
|
37
|
+
"Connection",
|
|
38
|
+
"new_connection",
|
|
39
|
+
"new_connection_with_operations",
|
|
40
|
+
"new_project_connection",
|
|
41
|
+
"Comment",
|
|
42
|
+
"Contact",
|
|
43
|
+
"ContactHandle",
|
|
44
|
+
"Conversation",
|
|
45
|
+
"ConversationDetails",
|
|
46
|
+
"ConversationReply",
|
|
47
|
+
"FindContactByEmailInput",
|
|
48
|
+
"FindContactByEmailOutput",
|
|
49
|
+
"GetConversationInput",
|
|
50
|
+
"Message",
|
|
51
|
+
"ReplyToConversationInput",
|
|
52
|
+
"SearchConversationsInput",
|
|
53
|
+
"SearchConversationsOutput",
|
|
54
|
+
"UpdateConversationInput",
|
|
55
|
+
"UpdateConversationOutput",
|
|
56
|
+
"build_reply_html",
|
|
57
|
+
"new_find_contact_by_email_step",
|
|
58
|
+
"new_get_conversation_step",
|
|
59
|
+
"new_reply_to_conversation_step",
|
|
60
|
+
"new_search_conversations_step",
|
|
61
|
+
"new_update_conversation_step",
|
|
62
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import SecretString
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Config:
|
|
12
|
+
|
|
13
|
+
max_response_bytes: int = 4194304
|
|
14
|
+
|
|
15
|
+
setattr(Config, '__module__', 'dex_connectors.front.client')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class Credentials:
|
|
20
|
+
|
|
21
|
+
api_token: SecretString
|
|
22
|
+
|
|
23
|
+
setattr(Credentials, '__module__', 'dex_connectors.front.client')
|