dex-connector-docusign 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.
Files changed (36) hide show
  1. dex_connector_docusign-0.1.0/.gitignore +16 -0
  2. dex_connector_docusign-0.1.0/LICENSE +21 -0
  3. dex_connector_docusign-0.1.0/PKG-INFO +100 -0
  4. dex_connector_docusign-0.1.0/README.md +87 -0
  5. dex_connector_docusign-0.1.0/examples/envelope_status/README.md +7 -0
  6. dex_connector_docusign-0.1.0/examples/envelope_status/__init__.py +3 -0
  7. dex_connector_docusign-0.1.0/examples/envelope_status/flow.py +76 -0
  8. dex_connector_docusign-0.1.0/examples/envelope_status/integration_test.py +197 -0
  9. dex_connector_docusign-0.1.0/examples/envelope_status/main.py +27 -0
  10. dex_connector_docusign-0.1.0/examples/envelope_status/sample-input.json +1 -0
  11. dex_connector_docusign-0.1.0/examples/envelope_status/start.py +59 -0
  12. dex_connector_docusign-0.1.0/examples/envelope_status/worker.py +28 -0
  13. dex_connector_docusign-0.1.0/pyproject.toml +41 -0
  14. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/__init__.py +17 -0
  15. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/__init__.py +2 -0
  16. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/config.py +31 -0
  17. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/connection.py +274 -0
  18. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/definitions.py +117 -0
  19. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/mock.py +326 -0
  20. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/models.py +318 -0
  21. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/refresh.py +9 -0
  22. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/steps.py +299 -0
  23. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/_generated/timestamps.py +60 -0
  24. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/client.py +585 -0
  25. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/connection.py +12 -0
  26. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/dex-connector.json +206 -0
  27. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/docusignmock/__init__.py +6 -0
  28. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/models.py +7 -0
  29. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/operations.py +1028 -0
  30. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/py.typed +2 -0
  31. dex_connector_docusign-0.1.0/src/dex_connectors/docusign/steps.py +5 -0
  32. dex_connector_docusign-0.1.0/tests/refresh_agreement_cases.json +706 -0
  33. dex_connector_docusign-0.1.0/tests/test_conformance.py +49 -0
  34. dex_connector_docusign-0.1.0/tests/test_operations.py +232 -0
  35. dex_connector_docusign-0.1.0/tests/test_refresh_agreement.py +278 -0
  36. dex_connector_docusign-0.1.0/uv.lock +934 -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,100 @@
1
+ Metadata-Version: 2.5
2
+ Name: dex-connector-docusign
3
+ Version: 0.1.0
4
+ Summary: DocuSign eSignature connector for Dex Flows
5
+ Project-URL: Source, https://github.com/superdurable/dex-connectors-library/tree/main/connectors/docusign/python
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: dex-connectors-sdk==0.7.0
10
+ Requires-Dist: dex-python-sdk>=1.28.0
11
+ Requires-Dist: httpx>=0.27
12
+ Description-Content-Type: text/markdown
13
+
14
+ # DocuSign eSignature Python connector
15
+
16
+ This package implements `createEnvelopeFromTemplate`, `getEnvelope`,
17
+ `listEnvelopeRecipients`, `voidEnvelope`, and `downloadCombinedDocument` for
18
+ Dex Python Flows. The `envelopeEventReceived` Connect Trigger remains Go-only.
19
+ The manifest owns operation identities, retry policies, branches, configuration,
20
+ and OAuth methods. The Python package version is `v0.1.0` and requires
21
+ `dex-connectors-sdk==0.7.0`.
22
+
23
+ `new_project_connection` loads the static connection name from Dex project
24
+ configuration. It uses the generated OAuth credential decoder and refresh
25
+ driver for production and developer accounts. Each provider call resolves the
26
+ current access token, reads `/oauth/userinfo` to choose the account and regional
27
+ API host, and rejects a host outside the selected environment. The PDF operation
28
+ returns a digest, byte count, and optional store location; PDF bytes never enter
29
+ a Dex Result. `CombinedDocumentStore` accepts bounded PDF bytes outside Dex and
30
+ must be idempotent and atomic.
31
+
32
+ ## Runnable example
33
+
34
+ [`examples/envelope_status/flow.py`](examples/envelope_status/flow.py) reads
35
+ an envelope with the static `docusign-esignature` connection. It persists only
36
+ the request and safe envelope summary. Its optional `notFound`,
37
+ `providerRejected`, `invalidResponse`, and `defect` branches are intentionally
38
+ unwired, so Dex fails the Flow if one is selected.
39
+
40
+ From `connectors/docusign/python`, install dependencies and produce a strict
41
+ FDG 2.0 file for Dex Web:
42
+
43
+ ```sh
44
+ uv sync --locked
45
+ dexcli visualize examples/envelope_status/flow.py --language python \
46
+ --schema-version 2.0 --lint app --json --out build/envelope-status \
47
+ --python .venv/bin/python
48
+ ```
49
+
50
+ Start `dexcli dev` with that rendering directory. In Dex Web **Connections**,
51
+ create `docusign / docusign-esignature`, choose Production account or Developer
52
+ account, and follow the authorization guide in the connector manifest. Register
53
+ Dex Web's exact redirect URI in the DocuSign app. The OAuth scopes are
54
+ `signature` and `extended`; Dex handles access and refresh tokens. The account
55
+ ID may stay blank to use the authorizing user's default account. A Connect HMAC
56
+ key is optional for this read-only example. Use the owner-only
57
+ `connections.json` exported by Dex Web as `DEX_CONNECTOR_CONFIG_FILE`.
58
+
59
+ Run the Worker and start a Flow with the included sample input:
60
+
61
+ ```sh
62
+ DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:40901 \
63
+ DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
64
+ uv run python -m examples.envelope_status.main
65
+
66
+ DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:40901 \
67
+ DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
68
+ uv run python -m examples.envelope_status.start \
69
+ docusign-envelope-001 examples/envelope_status/sample-input.json
70
+ ```
71
+
72
+ The Worker binding in [`main.py`](examples/envelope_status/main.py) is the
73
+ application entrypoint:
74
+
75
+ ```python
76
+ project = await load_from_environment()
77
+ connection = new_project_connection(project, CONNECTION_NAME)
78
+ try:
79
+ await run_worker(EnvelopeStatusFlow(connection))
80
+ finally:
81
+ await connection.aclose()
82
+ await project.close()
83
+ ```
84
+
85
+ Run the real Dex integration test against a local `dexcli dev` server:
86
+
87
+ ```sh
88
+ DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:40901 \
89
+ uv run pytest examples/envelope_status/integration_test.py
90
+ ```
91
+
92
+ The test binds its HTTP provider stand-in to an OS-assigned free port. It checks
93
+ retry after a 429 and failure on an unwired `notFound` branch through a real Dex
94
+ Worker and Client. The 52 Go-recorded conformance scenarios exercise every
95
+ operation's provider requests and exact SDK-encoded outputs. OAuth refresh
96
+ agreement has its own generated Go and Python suites.
97
+
98
+ Live DocuSign authorization, account selection, regional host resolution,
99
+ provider error codes, envelope read-back timing, PDF downloads, and Connect
100
+ configuration have not been verified with a real DocuSign account.
@@ -0,0 +1,87 @@
1
+ # DocuSign eSignature Python connector
2
+
3
+ This package implements `createEnvelopeFromTemplate`, `getEnvelope`,
4
+ `listEnvelopeRecipients`, `voidEnvelope`, and `downloadCombinedDocument` for
5
+ Dex Python Flows. The `envelopeEventReceived` Connect Trigger remains Go-only.
6
+ The manifest owns operation identities, retry policies, branches, configuration,
7
+ and OAuth methods. The Python package version is `v0.1.0` and requires
8
+ `dex-connectors-sdk==0.7.0`.
9
+
10
+ `new_project_connection` loads the static connection name from Dex project
11
+ configuration. It uses the generated OAuth credential decoder and refresh
12
+ driver for production and developer accounts. Each provider call resolves the
13
+ current access token, reads `/oauth/userinfo` to choose the account and regional
14
+ API host, and rejects a host outside the selected environment. The PDF operation
15
+ returns a digest, byte count, and optional store location; PDF bytes never enter
16
+ a Dex Result. `CombinedDocumentStore` accepts bounded PDF bytes outside Dex and
17
+ must be idempotent and atomic.
18
+
19
+ ## Runnable example
20
+
21
+ [`examples/envelope_status/flow.py`](examples/envelope_status/flow.py) reads
22
+ an envelope with the static `docusign-esignature` connection. It persists only
23
+ the request and safe envelope summary. Its optional `notFound`,
24
+ `providerRejected`, `invalidResponse`, and `defect` branches are intentionally
25
+ unwired, so Dex fails the Flow if one is selected.
26
+
27
+ From `connectors/docusign/python`, install dependencies and produce a strict
28
+ FDG 2.0 file for Dex Web:
29
+
30
+ ```sh
31
+ uv sync --locked
32
+ dexcli visualize examples/envelope_status/flow.py --language python \
33
+ --schema-version 2.0 --lint app --json --out build/envelope-status \
34
+ --python .venv/bin/python
35
+ ```
36
+
37
+ Start `dexcli dev` with that rendering directory. In Dex Web **Connections**,
38
+ create `docusign / docusign-esignature`, choose Production account or Developer
39
+ account, and follow the authorization guide in the connector manifest. Register
40
+ Dex Web's exact redirect URI in the DocuSign app. The OAuth scopes are
41
+ `signature` and `extended`; Dex handles access and refresh tokens. The account
42
+ ID may stay blank to use the authorizing user's default account. A Connect HMAC
43
+ key is optional for this read-only example. Use the owner-only
44
+ `connections.json` exported by Dex Web as `DEX_CONNECTOR_CONFIG_FILE`.
45
+
46
+ Run the Worker and start a Flow with the included sample input:
47
+
48
+ ```sh
49
+ DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:40901 \
50
+ DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
51
+ uv run python -m examples.envelope_status.main
52
+
53
+ DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:40901 \
54
+ DEX_CONNECTOR_CONFIG_FILE=/path/to/connections.json \
55
+ uv run python -m examples.envelope_status.start \
56
+ docusign-envelope-001 examples/envelope_status/sample-input.json
57
+ ```
58
+
59
+ The Worker binding in [`main.py`](examples/envelope_status/main.py) is the
60
+ application entrypoint:
61
+
62
+ ```python
63
+ project = await load_from_environment()
64
+ connection = new_project_connection(project, CONNECTION_NAME)
65
+ try:
66
+ await run_worker(EnvelopeStatusFlow(connection))
67
+ finally:
68
+ await connection.aclose()
69
+ await project.close()
70
+ ```
71
+
72
+ Run the real Dex integration test against a local `dexcli dev` server:
73
+
74
+ ```sh
75
+ DEX_FLOW_SERVICE_ADDRESS=127.0.0.1:40901 \
76
+ uv run pytest examples/envelope_status/integration_test.py
77
+ ```
78
+
79
+ The test binds its HTTP provider stand-in to an OS-assigned free port. It checks
80
+ retry after a 429 and failure on an unwired `notFound` branch through a real Dex
81
+ Worker and Client. The 52 Go-recorded conformance scenarios exercise every
82
+ operation's provider requests and exact SDK-encoded outputs. OAuth refresh
83
+ agreement has its own generated Go and Python suites.
84
+
85
+ Live DocuSign authorization, account selection, regional host resolution,
86
+ provider error codes, envelope read-back timing, PDF downloads, and Connect
87
+ configuration have not been verified with a real DocuSign account.
@@ -0,0 +1,7 @@
1
+ # Envelope status example
2
+
3
+ Run the strict FDG 2.0 generation, Dex Web connection setup, Worker, Client
4
+ start command, and real Dex integration test in the
5
+ [Python package README](../../README.md#runnable-example). The Flow uses
6
+ `docusign-esignature` and the `getEnvelope` Query. Its optional branches fail
7
+ the Flow when selected.
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """DocuSign envelope status example."""
@@ -0,0 +1,76 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Read a DocuSign envelope through one durable Dex Query Step."""
4
+
5
+ import dex
6
+ from dex_connectors.sdk import QueryResult
7
+
8
+ from dex_connectors.docusign import Connection, Envelope, GetEnvelopeInput, new_get_envelope_step
9
+
10
+ CONNECTION_NAME = "docusign-esignature"
11
+ FLOW_TYPE = "DocuSignEnvelopeStatusPython"
12
+ _OPERATION_STEP_TYPE = "GetDocuSignEnvelope"
13
+ _REQUEST = dex.Attribute("docusign-python-envelope-request", GetEnvelopeInput)
14
+ _ENVELOPE = dex.Attribute("docusign-python-envelope", Envelope)
15
+
16
+
17
+ def map_to_operation_input(request: GetEnvelopeInput) -> GetEnvelopeInput:
18
+ """Pass the typed request to the provider operation."""
19
+ return request
20
+
21
+
22
+ # dex:group group-id:docusign-envelope group-label:"DocuSign envelope"
23
+ # dex:explanation text:"Persist the envelope ID before reading DocuSign."
24
+ class _StartEnvelopeStatus(dex.Step[GetEnvelopeInput]):
25
+ def get_step_type(self) -> str:
26
+ return "StartDocuSignEnvelopeStatus"
27
+
28
+ def execute(self, context: dex.Context, input: GetEnvelopeInput) -> dex.StepDecision:
29
+ _REQUEST.set(context, input)
30
+ return dex.go_to(dex.StepRef("GetDocuSignEnvelope"), input)
31
+
32
+
33
+ # dex:group group-id:docusign-envelope group-label:"DocuSign envelope"
34
+ # dex:explanation text:"Persist the envelope status without secret or document bytes."
35
+ class _CompleteEnvelopeStatus(dex.Step[QueryResult[Envelope]]):
36
+ def get_step_type(self) -> str:
37
+ return "CompleteDocuSignEnvelopeStatus"
38
+
39
+ def execute(self, context: dex.Context, input: QueryResult[Envelope]) -> dex.StepDecision:
40
+ _ENVELOPE.set(context, input.value)
41
+ return dex.graceful_complete(input.value)
42
+
43
+
44
+ class EnvelopeStatusFlow(dex.Flow[GetEnvelopeInput]):
45
+ """Read one envelope through the static DocuSign connection."""
46
+
47
+ def __init__(self, connection: Connection) -> None:
48
+ if connection.reference.name != CONNECTION_NAME:
49
+ raise ValueError("envelope status requires the static DocuSign connection")
50
+ self._start = _StartEnvelopeStatus()
51
+ self._complete = _CompleteEnvelopeStatus()
52
+ self._get = new_get_envelope_step(
53
+ step_type=_OPERATION_STEP_TYPE,
54
+ step_input_type=GetEnvelopeInput,
55
+ annotations={
56
+ "groupId": "docusign-envelope",
57
+ "groupLabel": "DocuSign envelope",
58
+ "explanation": "Read the envelope status and text custom fields from DocuSign.",
59
+ },
60
+ connection=connection,
61
+ connection_name=CONNECTION_NAME,
62
+ map_to_operation_input=map_to_operation_input,
63
+ found=self._complete,
64
+ )
65
+
66
+ def get_flow_type(self) -> str:
67
+ """Return this example's stable Flow identity."""
68
+ return "DocuSignEnvelopeStatusPython"
69
+
70
+ def get_steps(self) -> dex.StepList[GetEnvelopeInput]:
71
+ """Register the start, provider query, and completion Steps."""
72
+ return dex.StepList.start_step(self._start).other_steps(self._get, self._complete)
73
+
74
+ def get_persistence_schema(self) -> dex.PersistenceSchema:
75
+ """Register both durable, non-secret Attributes."""
76
+ return dex.PersistenceSchema.of(_REQUEST, _ENVELOPE)
@@ -0,0 +1,197 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Verify the example through a real Dex Worker, Client, retry, and branch route."""
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import tempfile
9
+ import threading
10
+ from datetime import timedelta
11
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
12
+ from pathlib import Path
13
+ from urllib.parse import urlsplit
14
+ from uuid import uuid4
15
+
16
+ import dex
17
+ import httpx
18
+ import pytest
19
+ from dex_connectors.sdk.projectconfig import load_from_environment
20
+
21
+ from dex_connectors.docusign import Envelope, GetEnvelopeInput, new_project_connection
22
+
23
+ from .flow import CONNECTION_NAME, EnvelopeStatusFlow
24
+
25
+ ACCOUNT_ID = "a4ec37d6-1111-2222-3333-143885c220e1"
26
+ ENVELOPE_ID = "93be49ab-1111-2222-3333-f752070d71ec"
27
+
28
+
29
+ class _ProviderStandIn(BaseHTTPRequestHandler):
30
+ requests: list[str] = []
31
+
32
+ def do_GET(self) -> None:
33
+ """Serve one account, a rate limited first read, and a later envelope."""
34
+ self.requests.append(self.path)
35
+ body: dict[str, object]
36
+ if urlsplit(self.path).path == "/oauth/userinfo":
37
+ status, body = (
38
+ 200,
39
+ {
40
+ "accounts": [
41
+ {"account_id": ACCOUNT_ID, "is_default": True, "base_uri": "https://na3.docusign.net"}
42
+ ]
43
+ },
44
+ )
45
+ elif "000000000404" in self.path:
46
+ status, body = 404, {"errorCode": "ENVELOPE_DOES_NOT_EXIST"}
47
+ elif sum("/envelopes/" in path and ENVELOPE_ID in path for path in self.requests) == 1:
48
+ status, body = 429, {"errorCode": "RATE_LIMITED"}
49
+ else:
50
+ status, body = (
51
+ 200,
52
+ {
53
+ "envelopeId": ENVELOPE_ID,
54
+ "status": "completed",
55
+ "completedDateTime": "2026-10-04T12:00:00Z",
56
+ },
57
+ )
58
+ encoded = json.dumps(body).encode()
59
+ self.send_response(status)
60
+ self.send_header("Content-Type", "application/json")
61
+ self.send_header("Content-Length", str(len(encoded)))
62
+ self.end_headers()
63
+ self.wfile.write(encoded)
64
+
65
+ def do_POST(self) -> None:
66
+ """Refresh an inert OAuth token using the manifest's generated driver."""
67
+ self.requests.append(self.path)
68
+ length = int(self.headers.get("Content-Length", "0"))
69
+ self.rfile.read(length)
70
+ encoded = json.dumps(
71
+ {
72
+ "access_token": "inert-refreshed-access-token",
73
+ "refresh_token": "inert-refreshed-refresh-token",
74
+ "token_type": "Bearer",
75
+ "scope": "signature extended",
76
+ "expires_in": 3600,
77
+ }
78
+ ).encode()
79
+ self.send_response(200)
80
+ self.send_header("Content-Type", "application/json")
81
+ self.send_header("Content-Length", str(len(encoded)))
82
+ self.end_headers()
83
+ self.wfile.write(encoded)
84
+
85
+ def log_message(self, _format: str, *args: object) -> None:
86
+ """Keep the test log free of request headers."""
87
+
88
+
89
+ class _LocalProviderTransport(httpx.AsyncBaseTransport):
90
+ def __init__(self, port: int) -> None:
91
+ self.port = port
92
+ self.transport = httpx.AsyncHTTPTransport()
93
+
94
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
95
+ """Preserve the provider path while sending bytes to the local stand-in."""
96
+ local_url = f"http://127.0.0.1:{self.port}{request.url.raw_path.decode()}"
97
+ headers = [(name, value) for name, value in request.headers.raw if name.lower() != b"host"]
98
+ local_request = httpx.Request(
99
+ request.method, local_url, headers=headers, content=await request.aread()
100
+ )
101
+ return await self.transport.handle_async_request(local_request)
102
+
103
+ async def aclose(self) -> None:
104
+ """Close the owned HTTP transport."""
105
+ await self.transport.aclose()
106
+
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_envelope_status_retries_and_fails_unwired_not_found(
110
+ monkeypatch: pytest.MonkeyPatch,
111
+ ) -> None:
112
+ """Route the happy path and optional failure through Dex persistence."""
113
+ server_address = os.environ["DEX_FLOW_SERVICE_ADDRESS"]
114
+ _ProviderStandIn.requests = []
115
+ provider = ThreadingHTTPServer(("127.0.0.1", 0), _ProviderStandIn)
116
+ server_thread = threading.Thread(target=provider.serve_forever, daemon=True)
117
+ server_thread.start()
118
+ try:
119
+ async with httpx.AsyncClient(
120
+ transport=_LocalProviderTransport(provider.server_port)
121
+ ) as provider_http:
122
+ with tempfile.TemporaryDirectory(prefix="docusign-python-dex-") as directory:
123
+ connections_path = Path(directory) / "connections.json"
124
+ connections_path.write_text(
125
+ json.dumps(
126
+ {
127
+ "schemaVersion": "connectors.dex.dev/local-connections/v1alpha1",
128
+ "connections": [
129
+ {
130
+ "connectorId": "docusign",
131
+ "connectionName": CONNECTION_NAME,
132
+ "modulePath": (
133
+ "github.com/superdurable/dex-connectors-library/"
134
+ "connectors/docusign"
135
+ ),
136
+ "provider": "docusign",
137
+ "authMethodId": "docusign-oauth",
138
+ "configuration": {},
139
+ "credentials": {
140
+ "oauth_client_id": "inert-integration-key",
141
+ "oauth_client_secret": "inert-client-secret",
142
+ "access_token": "inert-access-token",
143
+ "refresh_token": "inert-refresh-token",
144
+ },
145
+ }
146
+ ],
147
+ "triggerBindings": [],
148
+ }
149
+ )
150
+ )
151
+ connections_path.chmod(0o600)
152
+ monkeypatch.setenv("DEX_CONNECTOR_CONFIG_FILE", str(connections_path))
153
+ project = await load_from_environment()
154
+ connection = new_project_connection(project, CONNECTION_NAME, provider_http)
155
+ flow = EnvelopeStatusFlow(connection)
156
+ registry = dex.Registry([flow], allow_async_handlers=True)
157
+ cache = dex.open_blob_cache(dex.BlobCacheConfig(str(Path(directory) / "blobs"), 1 << 24))
158
+ try:
159
+ worker = dex.AsyncWorker(
160
+ registry, cache, dex.WorkerOptions(server_address=server_address)
161
+ )
162
+ worker_task = asyncio.create_task(worker.start())
163
+ try:
164
+ await worker.wait_pull_registered(timeout=15)
165
+ async with dex.AsyncClient(
166
+ registry, cache, dex.ClientOptions(server_address=server_address)
167
+ ) as dex_client:
168
+ flow_id = f"docusign-python-{uuid4().hex}"
169
+ await dex_client.start_flow(flow, flow_id, GetEnvelopeInput(ENVELOPE_ID))
170
+ result = await asyncio.wait_for(
171
+ dex_client.wait_for_flow(flow_id, timedelta(seconds=30)), 40
172
+ )
173
+ assert result.status == dex.FlowStatus.COMPLETED, result.error_message
174
+ assert result.single_output(Envelope).status == "completed"
175
+ assert "/oauth/token" in _ProviderStandIn.requests
176
+ assert sum(ENVELOPE_ID in path for path in _ProviderStandIn.requests) == 2
177
+
178
+ missing_id = "93be49ab-1111-2222-3333-000000000404"
179
+ failed_id = f"docusign-python-{uuid4().hex}"
180
+ await dex_client.start_flow(flow, failed_id, GetEnvelopeInput(missing_id))
181
+ failed = await asyncio.wait_for(
182
+ dex_client.wait_for_flow(failed_id, timedelta(seconds=30)), 40
183
+ )
184
+ assert failed.status == dex.FlowStatus.FAILED
185
+ assert failed.error_message is not None
186
+ assert "notFound" in failed.error_message
187
+ finally:
188
+ await worker.stop()
189
+ await worker_task
190
+ finally:
191
+ cache.close()
192
+ await connection.aclose()
193
+ await project.close()
194
+ finally:
195
+ provider.shutdown()
196
+ provider.server_close()
197
+ server_thread.join(timeout=5)
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Load the DocuSign project connection and run the example Worker."""
4
+
5
+ import asyncio
6
+
7
+ from dex_connectors.sdk.projectconfig import load_from_environment
8
+
9
+ from dex_connectors.docusign import new_project_connection
10
+
11
+ from .flow import CONNECTION_NAME, EnvelopeStatusFlow
12
+ from .worker import run_worker
13
+
14
+
15
+ async def main() -> None:
16
+ """Bind the user-managed connection before Worker registration."""
17
+ project = await load_from_environment()
18
+ connection = new_project_connection(project, CONNECTION_NAME)
19
+ try:
20
+ await run_worker(EnvelopeStatusFlow(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
+ {"envelopeId":"93be49ab-1111-2222-3333-f752070d71ec"}
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Start the DocuSign envelope status Flow with a unique Flow ID."""
4
+
5
+ import argparse
6
+ import asyncio
7
+ import os
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ import dex
12
+ from dex_connectors.sdk.projectconfig import load_from_environment
13
+
14
+ from dex_connectors.docusign import GetEnvelopeInput, new_project_connection
15
+
16
+ from .flow import CONNECTION_NAME, EnvelopeStatusFlow
17
+
18
+
19
+ async def start_envelope_status(flow_id: str, input_file: Path) -> str:
20
+ """Decode one safe envelope ID and start the registered Dex Flow."""
21
+ encoded_input = input_file.read_bytes()
22
+ if len(encoded_input) > 4096:
23
+ raise ValueError("envelope status input exceeds 4096 bytes")
24
+ value = (
25
+ dex.CodecRegistry()
26
+ .resolve(GetEnvelopeInput)
27
+ .decode(dex.Value(dex.WireKind.JSON, encoded_input.decode("utf-8")))
28
+ )
29
+ project = await load_from_environment()
30
+ connection = new_project_connection(project, CONNECTION_NAME)
31
+ try:
32
+ flow = EnvelopeStatusFlow(connection)
33
+ registry = dex.Registry([flow], allow_async_handlers=True)
34
+ with tempfile.TemporaryDirectory(prefix="docusign-python-start-") as directory:
35
+ cache = dex.open_blob_cache(dex.BlobCacheConfig(directory, 1 << 24))
36
+ try:
37
+ address = os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")
38
+ async with dex.AsyncClient(
39
+ registry, cache, dex.ClientOptions(server_address=address)
40
+ ) as client:
41
+ return await client.start_flow(flow, flow_id, value)
42
+ finally:
43
+ cache.close()
44
+ finally:
45
+ await connection.aclose()
46
+ await project.close()
47
+
48
+
49
+ def main() -> None:
50
+ """Accept a Flow ID and a JSON input path from the command line."""
51
+ parser = argparse.ArgumentParser(description="Read a DocuSign envelope in Dex")
52
+ parser.add_argument("flow_id")
53
+ parser.add_argument("input_file", type=Path)
54
+ arguments = parser.parse_args()
55
+ print(asyncio.run(start_envelope_status(arguments.flow_id, arguments.input_file)))
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Run the DocuSign example with an asynchronous Dex Worker."""
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import dex
10
+
11
+ from .flow import EnvelopeStatusFlow
12
+
13
+
14
+ async def run_worker(flow: EnvelopeStatusFlow) -> None:
15
+ """Serve the registered example Flow until shutdown."""
16
+ registry = dex.Registry([flow], allow_async_handlers=True)
17
+ cache_directory = os.environ.get(
18
+ "DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-docusign-python-blobs")
19
+ )
20
+ cache = dex.open_blob_cache(dex.BlobCacheConfig(cache_directory, 1 << 30))
21
+ try:
22
+ options = dex.WorkerOptions(
23
+ server_address=os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")
24
+ )
25
+ async with dex.AsyncWorker(registry, cache, options) as worker:
26
+ await worker.start()
27
+ finally:
28
+ 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-docusign"
7
+ version = "0.1.0"
8
+ description = "DocuSign eSignature connector for Dex Flows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = ["dex-connectors-sdk==0.7.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/docusign/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"]