signingflow 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.
- signingflow-0.1.0/LICENSE +21 -0
- signingflow-0.1.0/PKG-INFO +117 -0
- signingflow-0.1.0/README.md +104 -0
- signingflow-0.1.0/pyproject.toml +29 -0
- signingflow-0.1.0/setup.cfg +4 -0
- signingflow-0.1.0/src/signingflow/__init__.py +35 -0
- signingflow-0.1.0/src/signingflow/allowlist.py +29 -0
- signingflow-0.1.0/src/signingflow/client.py +19 -0
- signingflow-0.1.0/src/signingflow/clock.py +13 -0
- signingflow-0.1.0/src/signingflow/config.py +105 -0
- signingflow-0.1.0/src/signingflow/envelopes.py +279 -0
- signingflow-0.1.0/src/signingflow/errors.py +79 -0
- signingflow-0.1.0/src/signingflow/files.py +81 -0
- signingflow-0.1.0/src/signingflow/jail.py +94 -0
- signingflow-0.1.0/src/signingflow/models.py +188 -0
- signingflow-0.1.0/src/signingflow/pagination.py +63 -0
- signingflow-0.1.0/src/signingflow/path.py +139 -0
- signingflow-0.1.0/src/signingflow/redact.py +46 -0
- signingflow-0.1.0/src/signingflow/request.py +247 -0
- signingflow-0.1.0/src/signingflow/resources/allowlist.json +110 -0
- signingflow-0.1.0/src/signingflow/status.py +111 -0
- signingflow-0.1.0/src/signingflow/templates.py +18 -0
- signingflow-0.1.0/src/signingflow/version.py +4 -0
- signingflow-0.1.0/src/signingflow/webhook_verify.py +49 -0
- signingflow-0.1.0/src/signingflow/webhooks.py +147 -0
- signingflow-0.1.0/src/signingflow.egg-info/PKG-INFO +117 -0
- signingflow-0.1.0/src/signingflow.egg-info/SOURCES.txt +29 -0
- signingflow-0.1.0/src/signingflow.egg-info/dependency_links.txt +1 -0
- signingflow-0.1.0/src/signingflow.egg-info/requires.txt +3 -0
- signingflow-0.1.0/src/signingflow.egg-info/top_level.txt +1 -0
- signingflow-0.1.0/tests/test_sdk.py +796 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Signingflow
|
|
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,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: signingflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for sending a document, reading envelope status, and verifying webhooks with the Signingflow API.
|
|
5
|
+
Author: Signingflow
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: coverage>=7.6; extra == "dev"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# signingflow
|
|
15
|
+
|
|
16
|
+
[](https://github.com/Signingflow/signingflow-python/actions/workflows/ci.yml)
|
|
17
|
+
[](https://github.com/Signingflow/signingflow-python/actions/workflows/ci.yml)
|
|
18
|
+
[](LICENSE)
|
|
19
|
+
|
|
20
|
+
Python SDK for the Signingflow calls used to send a document, read envelope status, and receive webhooks.
|
|
21
|
+
|
|
22
|
+
The package talks to `https://api.signingflow.com`. Keep the API key on your server. Python 3.11 or newer is required.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install "signingflow @ git+https://github.com/Signingflow/signingflow-python.git"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The package name is `signingflow`. Install it from [GitHub](https://github.com/Signingflow/signingflow-python) until it is published on PyPI.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
import signingflow
|
|
33
|
+
|
|
34
|
+
client = signingflow.Client(
|
|
35
|
+
os.environ["SIGNINGFLOW_API_KEY"],
|
|
36
|
+
workspace_id=os.environ["SIGNINGFLOW_WORKSPACE_ID"],
|
|
37
|
+
env="sandbox",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
envelope = client.envelopes.send_document(
|
|
41
|
+
title="NDA",
|
|
42
|
+
file=pdf_bytes,
|
|
43
|
+
filename="contract.pdf",
|
|
44
|
+
recipients=[{"name": "Ada Lovelace", "email": "ada@example.com"}],
|
|
45
|
+
message="Please sign",
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`file` is the raw bytes. `b""` is an empty file. A disk path is `path`, and it is read only from `upload_root`. That directory must be one this process writes. The open refuses a symlink, a hard link, an absolute path, and a path that leaves the directory. If this runtime cannot open a path without following a symlink, the read is refused. Do not pass a path from a request field.
|
|
50
|
+
|
|
51
|
+
`env` is `sandbox` or `live`. The client always sends `X-Request-Env`. A missing environment header is treated as live by the API, so the client rejects a missing environment.
|
|
52
|
+
|
|
53
|
+
`base_url` must be `https`. The host is `api.signingflow.com` unless `allow_custom_base_url` is set. A username or password on the URL is rejected. The host comparison is case-sensitive. There is no module-level API key.
|
|
54
|
+
|
|
55
|
+
The client sends `Accept-Encoding: identity`, does not follow redirects, and stops reading once an API response passes 8 MB.
|
|
56
|
+
|
|
57
|
+
## Send and read
|
|
58
|
+
|
|
59
|
+
`envelopes.prepare_draft` stores one PDF and creates a draft. It does not email anyone.
|
|
60
|
+
|
|
61
|
+
`envelopes.send_document` stores the PDF, creates the draft, adds the recipients to the first document, and sends. `envelopes.send_from_template` starts from `templates/{id}`.
|
|
62
|
+
|
|
63
|
+
If update or send fails, the error includes `envelope_name` for the draft that was saved. After a timeout, the client returns the envelope only when `sent_time` is within two minutes after the call started and not more than a minute ahead of this clock. An older or far-future timestamp is an error.
|
|
64
|
+
|
|
65
|
+
`envelopes.list(directory="needAction")` reads pages of 10. `page_size` can be from 1 to 1000. Omitting it selects 10. `0` is rejected. `all()` walks later pages and stops after 100 follow-up pages or a repeated token.
|
|
66
|
+
|
|
67
|
+
`envelopes.download_completed(name)` reads the envelope first. It requests the download URL only when the state is `COMPLETED`. The download call returns `uri`, `download_time`, and `etag`. This client does not GET `uri`. Treat that URL as untrusted until your application checks the host. Do not send the API key to it.
|
|
68
|
+
|
|
69
|
+
`envelopes.remind(name)` notifies people who can still sign. The call is not idempotent.
|
|
70
|
+
|
|
71
|
+
## Idempotency
|
|
72
|
+
|
|
73
|
+
Creates and sends send a UUID `idempotencyKey` in the JSON body. The same key is stored for the whole environment. A repeat returns HTTP 409 and does not return the original resource. The SDK raises `signingflow.IdempotencyError` with `resource_name` when the error includes one. It does not treat that response as success.
|
|
74
|
+
|
|
75
|
+
Pass the same `idempotency_key` only to retry one request after a timeout. `update` and `remind` are not retried automatically. A query value that contains the API key is rejected, including when that key is percent-encoded.
|
|
76
|
+
|
|
77
|
+
## Webhooks
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
destination = client.webhook_urls.create(
|
|
81
|
+
display_name="Envelope events",
|
|
82
|
+
http_request_uri="https://example.com/webhooks/signingflow",
|
|
83
|
+
events=["DOC_ENVELOPE_COMPLETED"],
|
|
84
|
+
)
|
|
85
|
+
secret = signingflow.signing_secret_of(destination)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Store that secret from the create response. A later `retrieve` does not include it. Do not set `http_config`. `auth` `NONE` is rejected.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
event = signingflow.construct_event(
|
|
92
|
+
raw_body,
|
|
93
|
+
signature_header,
|
|
94
|
+
secret,
|
|
95
|
+
previous_secret=previous_secret,
|
|
96
|
+
previous_signature=previous_signature,
|
|
97
|
+
)
|
|
98
|
+
event.payload
|
|
99
|
+
event.body_sha256
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`raw_body` must be the original `bytes`. The signature is lowercase hex HMAC-SHA256 of those bytes. There is no timestamp. Compare `X-Signingflow-Signature` or `X-Webhook-Signature`. During a grace window, also pass the previous header and secret. Parse JSON only after the MAC matches. `body_sha256` is the SHA-256 of those raw bytes. Store it and reject a delivery you have already seen.
|
|
103
|
+
|
|
104
|
+
## Errors
|
|
105
|
+
|
|
106
|
+
Every SDK error is a `signingflow.Error`.
|
|
107
|
+
|
|
108
|
+
| Type | When |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `AuthenticationError` | HTTP 401 |
|
|
111
|
+
| `PermissionError` | HTTP 403 |
|
|
112
|
+
| `IdempotencyError` | HTTP 409 whose body says the key was already used |
|
|
113
|
+
| `RateLimitError` | HTTP 429 |
|
|
114
|
+
| `InvalidRequestError` | Any other HTTP 4xx, or a rejected argument |
|
|
115
|
+
| `APIError` | HTTP 5xx, or a body the client will not parse |
|
|
116
|
+
| `ConnectionError` | The API was not reached, including a timeout |
|
|
117
|
+
| `SignatureVerificationError` | A webhook MAC did not match |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# signingflow
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Signingflow/signingflow-python/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Signingflow/signingflow-python/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Python SDK for the Signingflow calls used to send a document, read envelope status, and receive webhooks.
|
|
8
|
+
|
|
9
|
+
The package talks to `https://api.signingflow.com`. Keep the API key on your server. Python 3.11 or newer is required.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install "signingflow @ git+https://github.com/Signingflow/signingflow-python.git"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package name is `signingflow`. Install it from [GitHub](https://github.com/Signingflow/signingflow-python) until it is published on PyPI.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
import signingflow
|
|
20
|
+
|
|
21
|
+
client = signingflow.Client(
|
|
22
|
+
os.environ["SIGNINGFLOW_API_KEY"],
|
|
23
|
+
workspace_id=os.environ["SIGNINGFLOW_WORKSPACE_ID"],
|
|
24
|
+
env="sandbox",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
envelope = client.envelopes.send_document(
|
|
28
|
+
title="NDA",
|
|
29
|
+
file=pdf_bytes,
|
|
30
|
+
filename="contract.pdf",
|
|
31
|
+
recipients=[{"name": "Ada Lovelace", "email": "ada@example.com"}],
|
|
32
|
+
message="Please sign",
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`file` is the raw bytes. `b""` is an empty file. A disk path is `path`, and it is read only from `upload_root`. That directory must be one this process writes. The open refuses a symlink, a hard link, an absolute path, and a path that leaves the directory. If this runtime cannot open a path without following a symlink, the read is refused. Do not pass a path from a request field.
|
|
37
|
+
|
|
38
|
+
`env` is `sandbox` or `live`. The client always sends `X-Request-Env`. A missing environment header is treated as live by the API, so the client rejects a missing environment.
|
|
39
|
+
|
|
40
|
+
`base_url` must be `https`. The host is `api.signingflow.com` unless `allow_custom_base_url` is set. A username or password on the URL is rejected. The host comparison is case-sensitive. There is no module-level API key.
|
|
41
|
+
|
|
42
|
+
The client sends `Accept-Encoding: identity`, does not follow redirects, and stops reading once an API response passes 8 MB.
|
|
43
|
+
|
|
44
|
+
## Send and read
|
|
45
|
+
|
|
46
|
+
`envelopes.prepare_draft` stores one PDF and creates a draft. It does not email anyone.
|
|
47
|
+
|
|
48
|
+
`envelopes.send_document` stores the PDF, creates the draft, adds the recipients to the first document, and sends. `envelopes.send_from_template` starts from `templates/{id}`.
|
|
49
|
+
|
|
50
|
+
If update or send fails, the error includes `envelope_name` for the draft that was saved. After a timeout, the client returns the envelope only when `sent_time` is within two minutes after the call started and not more than a minute ahead of this clock. An older or far-future timestamp is an error.
|
|
51
|
+
|
|
52
|
+
`envelopes.list(directory="needAction")` reads pages of 10. `page_size` can be from 1 to 1000. Omitting it selects 10. `0` is rejected. `all()` walks later pages and stops after 100 follow-up pages or a repeated token.
|
|
53
|
+
|
|
54
|
+
`envelopes.download_completed(name)` reads the envelope first. It requests the download URL only when the state is `COMPLETED`. The download call returns `uri`, `download_time`, and `etag`. This client does not GET `uri`. Treat that URL as untrusted until your application checks the host. Do not send the API key to it.
|
|
55
|
+
|
|
56
|
+
`envelopes.remind(name)` notifies people who can still sign. The call is not idempotent.
|
|
57
|
+
|
|
58
|
+
## Idempotency
|
|
59
|
+
|
|
60
|
+
Creates and sends send a UUID `idempotencyKey` in the JSON body. The same key is stored for the whole environment. A repeat returns HTTP 409 and does not return the original resource. The SDK raises `signingflow.IdempotencyError` with `resource_name` when the error includes one. It does not treat that response as success.
|
|
61
|
+
|
|
62
|
+
Pass the same `idempotency_key` only to retry one request after a timeout. `update` and `remind` are not retried automatically. A query value that contains the API key is rejected, including when that key is percent-encoded.
|
|
63
|
+
|
|
64
|
+
## Webhooks
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
destination = client.webhook_urls.create(
|
|
68
|
+
display_name="Envelope events",
|
|
69
|
+
http_request_uri="https://example.com/webhooks/signingflow",
|
|
70
|
+
events=["DOC_ENVELOPE_COMPLETED"],
|
|
71
|
+
)
|
|
72
|
+
secret = signingflow.signing_secret_of(destination)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Store that secret from the create response. A later `retrieve` does not include it. Do not set `http_config`. `auth` `NONE` is rejected.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
event = signingflow.construct_event(
|
|
79
|
+
raw_body,
|
|
80
|
+
signature_header,
|
|
81
|
+
secret,
|
|
82
|
+
previous_secret=previous_secret,
|
|
83
|
+
previous_signature=previous_signature,
|
|
84
|
+
)
|
|
85
|
+
event.payload
|
|
86
|
+
event.body_sha256
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`raw_body` must be the original `bytes`. The signature is lowercase hex HMAC-SHA256 of those bytes. There is no timestamp. Compare `X-Signingflow-Signature` or `X-Webhook-Signature`. During a grace window, also pass the previous header and secret. Parse JSON only after the MAC matches. `body_sha256` is the SHA-256 of those raw bytes. Store it and reject a delivery you have already seen.
|
|
90
|
+
|
|
91
|
+
## Errors
|
|
92
|
+
|
|
93
|
+
Every SDK error is a `signingflow.Error`.
|
|
94
|
+
|
|
95
|
+
| Type | When |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `AuthenticationError` | HTTP 401 |
|
|
98
|
+
| `PermissionError` | HTTP 403 |
|
|
99
|
+
| `IdempotencyError` | HTTP 409 whose body says the key was already used |
|
|
100
|
+
| `RateLimitError` | HTTP 429 |
|
|
101
|
+
| `InvalidRequestError` | Any other HTTP 4xx, or a rejected argument |
|
|
102
|
+
| `APIError` | HTTP 5xx, or a body the client will not parse |
|
|
103
|
+
| `ConnectionError` | The API was not reached, including a timeout |
|
|
104
|
+
| `SignatureVerificationError` | A webhook MAC did not match |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "signingflow"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for sending a document, reading envelope status, and verifying webhooks with the Signingflow API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Signingflow" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = ["coverage>=7.6"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.package-data]
|
|
22
|
+
signingflow = ["resources/allowlist.json"]
|
|
23
|
+
|
|
24
|
+
[tool.coverage.run]
|
|
25
|
+
branch = true
|
|
26
|
+
source = ["signingflow"]
|
|
27
|
+
|
|
28
|
+
[tool.coverage.report]
|
|
29
|
+
show_missing = true
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Official Python SDK for the Signingflow calls used by skills and MCP tools."""
|
|
2
|
+
|
|
3
|
+
from signingflow.client import Client
|
|
4
|
+
from signingflow.errors import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ConnectionError,
|
|
8
|
+
Error,
|
|
9
|
+
IdempotencyError,
|
|
10
|
+
InvalidRequestError,
|
|
11
|
+
PermissionError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
SignatureVerificationError,
|
|
14
|
+
)
|
|
15
|
+
from signingflow.version import VERSION
|
|
16
|
+
from signingflow.webhook_verify import Event, construct_event
|
|
17
|
+
from signingflow.webhooks import HTTPConfig, signing_secret_of
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"VERSION",
|
|
21
|
+
"APIError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"Client",
|
|
24
|
+
"ConnectionError",
|
|
25
|
+
"Error",
|
|
26
|
+
"Event",
|
|
27
|
+
"HTTPConfig",
|
|
28
|
+
"IdempotencyError",
|
|
29
|
+
"InvalidRequestError",
|
|
30
|
+
"PermissionError",
|
|
31
|
+
"RateLimitError",
|
|
32
|
+
"SignatureVerificationError",
|
|
33
|
+
"construct_event",
|
|
34
|
+
"signing_secret_of",
|
|
35
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The 15 operations used by skills and MCP tools."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
|
|
7
|
+
from signingflow.errors import Error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load():
|
|
11
|
+
try:
|
|
12
|
+
raw = files("signingflow").joinpath("resources/allowlist.json").read_text(encoding="utf-8")
|
|
13
|
+
parsed = json.loads(raw)
|
|
14
|
+
operations = parsed["operations"]
|
|
15
|
+
if len(operations) != 15:
|
|
16
|
+
raise ValueError("allowlist length")
|
|
17
|
+
return {item["operationId"]: item for item in operations}
|
|
18
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
|
|
19
|
+
raise RuntimeError("allowlist.json is invalid") from exc
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
OPERATIONS = _load()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def operation(operation_id):
|
|
26
|
+
try:
|
|
27
|
+
return OPERATIONS[operation_id]
|
|
28
|
+
except KeyError as exc:
|
|
29
|
+
raise Error(f"Unknown operation {operation_id}.") from exc
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""The instance client. There is no module-level API key."""
|
|
2
|
+
|
|
3
|
+
from signingflow.config import Config
|
|
4
|
+
from signingflow.envelopes import Envelopes
|
|
5
|
+
from signingflow.files import Files
|
|
6
|
+
from signingflow.request import Sender
|
|
7
|
+
from signingflow.templates import Templates
|
|
8
|
+
from signingflow.webhooks import WebhookDeliveries, WebhookURLs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Client:
|
|
12
|
+
def __init__(self, api_key, **options):
|
|
13
|
+
self.config = Config(api_key, **options)
|
|
14
|
+
sender = Sender(self.config)
|
|
15
|
+
self.files = Files(sender)
|
|
16
|
+
self.templates = Templates(sender)
|
|
17
|
+
self.envelopes = Envelopes(sender, self.files, self.templates)
|
|
18
|
+
self.webhook_urls = WebhookURLs(sender)
|
|
19
|
+
self.webhook_deliveries = WebhookDeliveries(sender)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Client settings. The API key stays on the instance."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlsplit
|
|
4
|
+
|
|
5
|
+
OFFICIAL_HOST = "api.signingflow.com"
|
|
6
|
+
DEFAULT_BASE_URL = "https://api.signingflow.com"
|
|
7
|
+
DEFAULT_TIMEOUT = 30
|
|
8
|
+
DEFAULT_RETRIES = 2
|
|
9
|
+
DEFAULT_UPLOAD_HOSTS = ("storage.googleapis.com",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key,
|
|
16
|
+
*,
|
|
17
|
+
workspace_id,
|
|
18
|
+
env,
|
|
19
|
+
base_url=None,
|
|
20
|
+
allow_custom_base_url=False,
|
|
21
|
+
timeout=None,
|
|
22
|
+
max_network_retries=None,
|
|
23
|
+
channel=None,
|
|
24
|
+
funnel=None,
|
|
25
|
+
upload_hosts=None,
|
|
26
|
+
upload_root=None,
|
|
27
|
+
ssl_context=None,
|
|
28
|
+
):
|
|
29
|
+
from signingflow.errors import Error, invalid
|
|
30
|
+
|
|
31
|
+
self.api_key = (api_key or "").strip()
|
|
32
|
+
if not self.api_key:
|
|
33
|
+
raise Error("API key is required.")
|
|
34
|
+
self.workspace_id = (workspace_id or "").strip()
|
|
35
|
+
if not self.workspace_id:
|
|
36
|
+
raise Error("workspace_id is required.")
|
|
37
|
+
self.wire_env = _normalize_env(env)
|
|
38
|
+
base = (base_url or "").strip() or DEFAULT_BASE_URL
|
|
39
|
+
base = base.rstrip("/")
|
|
40
|
+
parsed = urlsplit(base)
|
|
41
|
+
if not parsed.scheme or not parsed.netloc:
|
|
42
|
+
raise Error("base_url must be an absolute URL.")
|
|
43
|
+
if parsed.scheme != "https":
|
|
44
|
+
raise Error("base_url must be https.")
|
|
45
|
+
if parsed.username is not None or parsed.password is not None:
|
|
46
|
+
raise Error("base_url must not include a username or password.")
|
|
47
|
+
if "\x00" in base:
|
|
48
|
+
raise Error("base_url must be an absolute URL.")
|
|
49
|
+
raw_host = _raw_host(parsed.netloc)
|
|
50
|
+
if raw_host != OFFICIAL_HOST and not allow_custom_base_url:
|
|
51
|
+
raise Error("base_url host must be api.signingflow.com, or set allow_custom_base_url.")
|
|
52
|
+
self.base_url = base
|
|
53
|
+
self.allow_custom_base_url = bool(allow_custom_base_url)
|
|
54
|
+
self.timeout = DEFAULT_TIMEOUT if timeout is None else timeout
|
|
55
|
+
if not isinstance(self.timeout, (int, float)) or isinstance(self.timeout, bool) or self.timeout <= 0:
|
|
56
|
+
raise Error("timeout must be positive.")
|
|
57
|
+
self.retries = DEFAULT_RETRIES if max_network_retries is None else max_network_retries
|
|
58
|
+
if isinstance(self.retries, bool) or not isinstance(self.retries, int) or self.retries < 0:
|
|
59
|
+
raise Error("max_network_retries must be a whole number of zero or more.")
|
|
60
|
+
self.upload_hosts = _upload_hosts(upload_hosts)
|
|
61
|
+
self.channel = (channel or "").strip()
|
|
62
|
+
self.funnel = (funnel or "").strip()
|
|
63
|
+
self.upload_root = (upload_root or "").strip()
|
|
64
|
+
self.ssl_context = ssl_context
|
|
65
|
+
if any(bad_header(value) for value in (self.api_key, self.workspace_id, self.channel, self.funnel)):
|
|
66
|
+
raise invalid("invalid header value")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def bad_header(value):
|
|
70
|
+
return "\r" in value or "\n" in value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_env(env):
|
|
74
|
+
from signingflow.errors import Error
|
|
75
|
+
|
|
76
|
+
if env in ("sandbox", "SANDBOX"):
|
|
77
|
+
return "SANDBOX"
|
|
78
|
+
if env in ("live", "LIVE"):
|
|
79
|
+
return "LIVE"
|
|
80
|
+
raise Error("env must be sandbox or live.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _raw_host(netloc):
|
|
84
|
+
host = netloc.rsplit("@", 1)[-1]
|
|
85
|
+
if host.startswith("["):
|
|
86
|
+
end = host.find("]")
|
|
87
|
+
return host[: end + 1] if end != -1 else host
|
|
88
|
+
return host.split(":", 1)[0]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _upload_hosts(hosts):
|
|
92
|
+
from signingflow.errors import Error
|
|
93
|
+
|
|
94
|
+
if hosts is None:
|
|
95
|
+
hosts = DEFAULT_UPLOAD_HOSTS
|
|
96
|
+
cleaned = []
|
|
97
|
+
for host in hosts:
|
|
98
|
+
host = str(host).strip().lower()
|
|
99
|
+
if not host:
|
|
100
|
+
continue
|
|
101
|
+
dots = host.count(".")
|
|
102
|
+
if dots < 2 or host.startswith(".") or host.endswith(".") or ".." in host:
|
|
103
|
+
raise Error("Each upload_hosts entry must be a specific host, not a public suffix.")
|
|
104
|
+
cleaned.append(host)
|
|
105
|
+
return tuple(cleaned)
|