signwell-sdk-python 0.1.0__py3-none-any.whl
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.
- signwell_sdk/__init__.py +287 -0
- signwell_sdk/api/__init__.py +10 -0
- signwell_sdk/api/api_application_api.py +481 -0
- signwell_sdk/api/bulk_send_api.py +1589 -0
- signwell_sdk/api/document_api.py +2425 -0
- signwell_sdk/api/me_api.py +246 -0
- signwell_sdk/api/regional_api.py +345 -0
- signwell_sdk/api/template_api.py +1298 -0
- signwell_sdk/api/webhooks_api.py +710 -0
- signwell_sdk/api_client.py +747 -0
- signwell_sdk/api_response.py +20 -0
- signwell_sdk/configuration.py +599 -0
- signwell_sdk/embedded.py +385 -0
- signwell_sdk/exceptions.py +314 -0
- signwell_sdk/models/__init__.py +114 -0
- signwell_sdk/models/account_info_response.py +143 -0
- signwell_sdk/models/account_info_response_active_users_inner.py +107 -0
- signwell_sdk/models/account_info_response_preferences.py +147 -0
- signwell_sdk/models/additional_fields_inner_inner.py +274 -0
- signwell_sdk/models/additional_fields_inner_inner_value.py +189 -0
- signwell_sdk/models/additional_files_inner.py +103 -0
- signwell_sdk/models/api_application_response.py +137 -0
- signwell_sdk/models/api_application_response_owner.py +107 -0
- signwell_sdk/models/api_application_response_preferences.py +135 -0
- signwell_sdk/models/attachment_request_info.py +99 -0
- signwell_sdk/models/attachment_requests_inner.py +110 -0
- signwell_sdk/models/bulk_send_create_response.py +132 -0
- signwell_sdk/models/bulk_send_csv_request.py +100 -0
- signwell_sdk/models/bulk_send_csv_template_response.py +97 -0
- signwell_sdk/models/bulk_send_documents_response.py +167 -0
- signwell_sdk/models/bulk_send_list_item.py +141 -0
- signwell_sdk/models/bulk_send_list_response.py +138 -0
- signwell_sdk/models/bulk_send_response.py +151 -0
- signwell_sdk/models/bulk_send_response_templates_inner.py +98 -0
- signwell_sdk/models/bulk_send_validate_csv_response.py +129 -0
- signwell_sdk/models/checkbox_group_info.py +138 -0
- signwell_sdk/models/checkbox_groups_inner.py +136 -0
- signwell_sdk/models/checkbox_validation.py +35 -0
- signwell_sdk/models/completed_pdf_response.py +177 -0
- signwell_sdk/models/completed_pdf_url_response.py +97 -0
- signwell_sdk/models/copied_contact_info.py +99 -0
- signwell_sdk/models/copied_contacts_inner.py +98 -0
- signwell_sdk/models/copied_placeholders_inner.py +111 -0
- signwell_sdk/models/create_bulk_send_request.py +155 -0
- signwell_sdk/models/create_webhook_request.py +100 -0
- signwell_sdk/models/date_format.py +36 -0
- signwell_sdk/models/detailed_option.py +105 -0
- signwell_sdk/models/document_from_template_request.py +377 -0
- signwell_sdk/models/document_from_template_response.py +353 -0
- signwell_sdk/models/document_from_template_response_fields_inner_inner.py +270 -0
- signwell_sdk/models/document_from_template_response_fields_inner_inner_recipient.py +99 -0
- signwell_sdk/models/document_from_template_response_recipients_inner.py +206 -0
- signwell_sdk/models/document_list_response.py +138 -0
- signwell_sdk/models/document_request.py +350 -0
- signwell_sdk/models/document_response.py +357 -0
- signwell_sdk/models/document_response_attachment_requests_inner.py +101 -0
- signwell_sdk/models/document_response_fields_inner_inner.py +268 -0
- signwell_sdk/models/document_response_fields_inner_inner_options_inner.py +101 -0
- signwell_sdk/models/document_response_fields_inner_inner_recipient.py +98 -0
- signwell_sdk/models/document_response_fields_inner_inner_value.py +195 -0
- signwell_sdk/models/document_response_recipients_inner.py +208 -0
- signwell_sdk/models/document_template_list_response.py +138 -0
- signwell_sdk/models/document_template_request.py +307 -0
- signwell_sdk/models/document_template_response.py +323 -0
- signwell_sdk/models/document_template_response_copied_placeholders_inner.py +131 -0
- signwell_sdk/models/document_template_response_placeholders_inner.py +144 -0
- signwell_sdk/models/document_template_update_request.py +208 -0
- signwell_sdk/models/dropdown_option.py +153 -0
- signwell_sdk/models/error_response.py +107 -0
- signwell_sdk/models/error_response_meta.py +101 -0
- signwell_sdk/models/field_type.py +46 -0
- signwell_sdk/models/fields_inner_inner.py +268 -0
- signwell_sdk/models/file_format.py +33 -0
- signwell_sdk/models/file_info.py +98 -0
- signwell_sdk/models/files_inner.py +103 -0
- signwell_sdk/models/label_info.py +98 -0
- signwell_sdk/models/label_request.py +97 -0
- signwell_sdk/models/label_response.py +98 -0
- signwell_sdk/models/me_response.py +130 -0
- signwell_sdk/models/me_response_contact.py +147 -0
- signwell_sdk/models/me_response_user.py +117 -0
- signwell_sdk/models/me_response_user_preferences.py +105 -0
- signwell_sdk/models/nom151_certificate_response.py +107 -0
- signwell_sdk/models/nom151_certificate_response_nom151.py +125 -0
- signwell_sdk/models/nom151_url_response.py +97 -0
- signwell_sdk/models/pagination_meta.py +119 -0
- signwell_sdk/models/placeholders_inner.py +115 -0
- signwell_sdk/models/rate_limit_error_response.py +97 -0
- signwell_sdk/models/recipients_inner.py +141 -0
- signwell_sdk/models/reminder_recipients_inner.py +100 -0
- signwell_sdk/models/send_reminder_request.py +114 -0
- signwell_sdk/models/template_attachment_requests_inner.py +110 -0
- signwell_sdk/models/template_checkbox_groups_inner.py +136 -0
- signwell_sdk/models/template_field_values_inner.py +109 -0
- signwell_sdk/models/template_field_values_inner_value.py +189 -0
- signwell_sdk/models/template_fields_inner_inner.py +266 -0
- signwell_sdk/models/template_recipients_inner.py +146 -0
- signwell_sdk/models/text_validation.py +42 -0
- signwell_sdk/models/update_document_and_send_request.py +234 -0
- signwell_sdk/models/update_recipients_map_inner.py +118 -0
- signwell_sdk/models/update_recipients_request.py +111 -0
- signwell_sdk/models/validation_error_response.py +99 -0
- signwell_sdk/models/webhook_response.py +110 -0
- signwell_sdk/py.typed +0 -0
- signwell_sdk/rest.py +209 -0
- signwell_sdk/webhook.py +214 -0
- signwell_sdk_python-0.1.0.dist-info/METADATA +177 -0
- signwell_sdk_python-0.1.0.dist-info/RECORD +110 -0
- signwell_sdk_python-0.1.0.dist-info/WHEEL +4 -0
- signwell_sdk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
signwell_sdk/embedded.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# Source: signwell-sdk-generator/extras/python/overlay/signwell_sdk/embedded.py
|
|
2
|
+
# Do not edit the generated SDK copy directly.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from signwell_sdk.api.document_api import DocumentApi
|
|
13
|
+
from signwell_sdk.api_client import ApiClient
|
|
14
|
+
from signwell_sdk.models.document_from_template_request import (
|
|
15
|
+
DocumentFromTemplateRequest,
|
|
16
|
+
)
|
|
17
|
+
from signwell_sdk.models.document_request import DocumentRequest
|
|
18
|
+
from signwell_sdk.models.fields_inner_inner import FieldsInnerInner
|
|
19
|
+
from signwell_sdk.models.files_inner import FilesInner
|
|
20
|
+
from signwell_sdk.models.recipients_inner import RecipientsInner
|
|
21
|
+
from signwell_sdk.models.template_recipients_inner import TemplateRecipientsInner
|
|
22
|
+
|
|
23
|
+
SCRIPT_URL = "https://static.signwell.com/assets/embedded.js"
|
|
24
|
+
SAFE_HANDLER_PATH = re.compile(r"^[$A-Z_][0-9A-Z_$]*(?:\.[$A-Z_][0-9A-Z_$]*)*$", re.IGNORECASE)
|
|
25
|
+
BLOCKED_HANDLER_SEGMENTS = {"__proto__", "constructor", "prototype"}
|
|
26
|
+
DEFAULT_SIGNWELL_HOSTS = {"www.signwell.com", "signwell.com"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_signing_document(
|
|
30
|
+
*,
|
|
31
|
+
name: str,
|
|
32
|
+
files: Sequence[Mapping[str, Any]],
|
|
33
|
+
recipients: Sequence[Mapping[str, Any]],
|
|
34
|
+
fields: Sequence[Sequence[Mapping[str, Any]]] | None = None,
|
|
35
|
+
test_mode: bool = False,
|
|
36
|
+
send_notifications: bool = False,
|
|
37
|
+
options: Mapping[str, Any] | None = None,
|
|
38
|
+
**attrs: Any,
|
|
39
|
+
) -> Any:
|
|
40
|
+
recipient_models = _build_recipients(recipients)
|
|
41
|
+
request_attrs = {
|
|
42
|
+
**attrs,
|
|
43
|
+
"name": name,
|
|
44
|
+
"test_mode": test_mode,
|
|
45
|
+
"files": _build_files(files),
|
|
46
|
+
"recipients": recipient_models,
|
|
47
|
+
"embedded_signing": True,
|
|
48
|
+
"embedded_signing_notifications": send_notifications,
|
|
49
|
+
}
|
|
50
|
+
if fields is not None:
|
|
51
|
+
request_attrs["fields"] = _build_fields(fields, recipient_models)
|
|
52
|
+
|
|
53
|
+
request = DocumentRequest(**request_attrs)
|
|
54
|
+
_validate_signing_placement(request, recipient_models)
|
|
55
|
+
return _document_api(options).create_document(request)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def create_requesting_document(
|
|
59
|
+
*,
|
|
60
|
+
name: str,
|
|
61
|
+
files: Sequence[Mapping[str, Any]],
|
|
62
|
+
recipients: Sequence[Mapping[str, Any]],
|
|
63
|
+
test_mode: bool = False,
|
|
64
|
+
options: Mapping[str, Any] | None = None,
|
|
65
|
+
**attrs: Any,
|
|
66
|
+
) -> Any:
|
|
67
|
+
request = DocumentRequest(
|
|
68
|
+
**attrs,
|
|
69
|
+
name=name,
|
|
70
|
+
test_mode=test_mode,
|
|
71
|
+
files=_build_files(files),
|
|
72
|
+
recipients=_build_recipients(recipients),
|
|
73
|
+
draft=True,
|
|
74
|
+
)
|
|
75
|
+
return _document_api(options).create_document(request)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_signing_document_from_template(
|
|
79
|
+
*,
|
|
80
|
+
recipients: Sequence[Mapping[str, Any]],
|
|
81
|
+
template_id: str | None = None,
|
|
82
|
+
template_ids: Sequence[str] | None = None,
|
|
83
|
+
test_mode: bool = False,
|
|
84
|
+
send_notifications: bool = False,
|
|
85
|
+
options: Mapping[str, Any] | None = None,
|
|
86
|
+
**attrs: Any,
|
|
87
|
+
) -> Any:
|
|
88
|
+
if template_id and template_ids:
|
|
89
|
+
raise ValueError("Provide either template_id or template_ids, not both")
|
|
90
|
+
if not template_id and not template_ids:
|
|
91
|
+
raise ValueError("Provide template_id or template_ids")
|
|
92
|
+
|
|
93
|
+
request_attrs = {
|
|
94
|
+
**attrs,
|
|
95
|
+
"test_mode": test_mode,
|
|
96
|
+
"recipients": _build_template_recipients(recipients),
|
|
97
|
+
"embedded_signing": True,
|
|
98
|
+
"embedded_signing_notifications": send_notifications,
|
|
99
|
+
}
|
|
100
|
+
if template_id:
|
|
101
|
+
request_attrs["template_id"] = template_id
|
|
102
|
+
if template_ids:
|
|
103
|
+
request_attrs["template_ids"] = list(template_ids)
|
|
104
|
+
|
|
105
|
+
request = DocumentFromTemplateRequest(**request_attrs)
|
|
106
|
+
return _document_api(options).create_document_from_template(request)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def embedded_signing_urls(document: Any) -> dict[str, str]:
|
|
110
|
+
recipients = getattr(document, "recipients", None) or []
|
|
111
|
+
urls: dict[str, str] = {}
|
|
112
|
+
for recipient in recipients:
|
|
113
|
+
email = getattr(recipient, "email", None)
|
|
114
|
+
url = getattr(recipient, "embedded_signing_url", None)
|
|
115
|
+
if email and url:
|
|
116
|
+
urls[email] = url
|
|
117
|
+
return urls
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def embedded_signing_url(document: Any, recipient_index: int = 0) -> str | None:
|
|
121
|
+
recipients = getattr(document, "recipients", None) or []
|
|
122
|
+
if recipient_index < 0 or recipient_index >= len(recipients):
|
|
123
|
+
return None
|
|
124
|
+
return getattr(recipients[recipient_index], "embedded_signing_url", None)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def script_tag() -> str:
|
|
128
|
+
return f'<script src="{SCRIPT_URL}"></script>'
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def signing_iframe(
|
|
132
|
+
*,
|
|
133
|
+
url: str,
|
|
134
|
+
allowed_embed_hosts: Sequence[str] | None = None,
|
|
135
|
+
allowed_redirect_hosts: Sequence[str] | None = None,
|
|
136
|
+
container_id: str | None = None,
|
|
137
|
+
allow_decline: bool | None = None,
|
|
138
|
+
allow_close: bool | None = None,
|
|
139
|
+
show_header: bool | None = None,
|
|
140
|
+
allow_download: bool | None = None,
|
|
141
|
+
redirect_url: str | None = None,
|
|
142
|
+
decline_redirect_url: str | None = None,
|
|
143
|
+
events: Mapping[str, str] | None = None,
|
|
144
|
+
auto_open: bool = True,
|
|
145
|
+
) -> str:
|
|
146
|
+
options = _build_iframe_options(
|
|
147
|
+
url=url,
|
|
148
|
+
allowed_embed_hosts=allowed_embed_hosts,
|
|
149
|
+
allowed_redirect_hosts=allowed_redirect_hosts,
|
|
150
|
+
values={"containerId": container_id},
|
|
151
|
+
booleans={
|
|
152
|
+
"allowDecline": allow_decline,
|
|
153
|
+
"allowClose": allow_close,
|
|
154
|
+
"showHeader": show_header,
|
|
155
|
+
"allowDownload": allow_download,
|
|
156
|
+
},
|
|
157
|
+
redirects={
|
|
158
|
+
"redirectUrl": redirect_url,
|
|
159
|
+
"declineRedirectUrl": decline_redirect_url,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
return _build_embed_script(options, events or {}, auto_open)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def requesting_iframe(
|
|
166
|
+
*,
|
|
167
|
+
url: str,
|
|
168
|
+
allowed_embed_hosts: Sequence[str] | None = None,
|
|
169
|
+
allowed_redirect_hosts: Sequence[str] | None = None,
|
|
170
|
+
container_id: str | None = None,
|
|
171
|
+
allow_close: bool | None = None,
|
|
172
|
+
show_header: bool | None = None,
|
|
173
|
+
allow_download: bool | None = None,
|
|
174
|
+
show_send_button: bool | None = None,
|
|
175
|
+
redirect_url: str | None = None,
|
|
176
|
+
events: Mapping[str, str] | None = None,
|
|
177
|
+
auto_open: bool = True,
|
|
178
|
+
) -> str:
|
|
179
|
+
options = _build_iframe_options(
|
|
180
|
+
url=url,
|
|
181
|
+
allowed_embed_hosts=allowed_embed_hosts,
|
|
182
|
+
allowed_redirect_hosts=allowed_redirect_hosts,
|
|
183
|
+
values={"containerId": container_id},
|
|
184
|
+
booleans={
|
|
185
|
+
"allowClose": allow_close,
|
|
186
|
+
"showHeader": show_header,
|
|
187
|
+
"allowDownload": allow_download,
|
|
188
|
+
"showSendButton": show_send_button,
|
|
189
|
+
},
|
|
190
|
+
redirects={"redirectUrl": redirect_url},
|
|
191
|
+
)
|
|
192
|
+
return _build_embed_script(options, events or {}, auto_open)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _document_api(options: Mapping[str, Any] | None) -> Any:
|
|
196
|
+
options = options or {}
|
|
197
|
+
if options.get("document_api") is not None:
|
|
198
|
+
return options["document_api"]
|
|
199
|
+
if options.get("api_client") is not None:
|
|
200
|
+
return DocumentApi(options["api_client"])
|
|
201
|
+
if options.get("configuration") is not None:
|
|
202
|
+
return DocumentApi(ApiClient(options["configuration"]))
|
|
203
|
+
return DocumentApi()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _build_recipients(recipients: Sequence[Mapping[str, Any]]) -> list[RecipientsInner]:
|
|
207
|
+
return [RecipientsInner(**_recipient_attrs(recipient, index)) for index, recipient in enumerate(recipients)]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _build_template_recipients(
|
|
211
|
+
recipients: Sequence[Mapping[str, Any]],
|
|
212
|
+
) -> list[TemplateRecipientsInner]:
|
|
213
|
+
built = []
|
|
214
|
+
for index, recipient in enumerate(recipients):
|
|
215
|
+
attrs = _recipient_attrs(recipient, index)
|
|
216
|
+
placeholder_name = _mapping_value(recipient, "placeholder_name")
|
|
217
|
+
if placeholder_name:
|
|
218
|
+
attrs["placeholder_name"] = placeholder_name
|
|
219
|
+
built.append(TemplateRecipientsInner(**attrs))
|
|
220
|
+
return built
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _build_files(files: Sequence[Mapping[str, Any]]) -> list[FilesInner]:
|
|
224
|
+
built = []
|
|
225
|
+
for file_input in files:
|
|
226
|
+
name = _mapping_value(file_input, "name")
|
|
227
|
+
file_url = _mapping_value(file_input, "file_url")
|
|
228
|
+
file_base64 = _mapping_value(file_input, "file_base64")
|
|
229
|
+
has_url = _present(file_url)
|
|
230
|
+
has_base64 = _present(file_base64)
|
|
231
|
+
|
|
232
|
+
if not _present(name):
|
|
233
|
+
raise ValueError("Each file must include name")
|
|
234
|
+
if has_url == has_base64:
|
|
235
|
+
raise ValueError("Each file must include exactly one of file_url or file_base64")
|
|
236
|
+
|
|
237
|
+
attrs = {"name": name}
|
|
238
|
+
attrs["file_url" if has_url else "file_base64"] = file_url if has_url else file_base64
|
|
239
|
+
built.append(FilesInner(**attrs))
|
|
240
|
+
return built
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _build_fields(
|
|
244
|
+
fields: Sequence[Sequence[Mapping[str, Any]]],
|
|
245
|
+
recipients: Sequence[RecipientsInner],
|
|
246
|
+
) -> list[list[FieldsInnerInner]]:
|
|
247
|
+
default_recipient_id = recipients[0].id if recipients else None
|
|
248
|
+
built = []
|
|
249
|
+
for file_fields in fields:
|
|
250
|
+
file_field_models = []
|
|
251
|
+
for field_input in file_fields:
|
|
252
|
+
attrs = dict(field_input)
|
|
253
|
+
attrs.setdefault("recipient_id", default_recipient_id)
|
|
254
|
+
attrs.setdefault("required", True)
|
|
255
|
+
if not attrs.get("recipient_id"):
|
|
256
|
+
raise ValueError("Each field must include recipient_id when no default recipient exists")
|
|
257
|
+
file_field_models.append(FieldsInnerInner(**attrs))
|
|
258
|
+
built.append(file_field_models)
|
|
259
|
+
return built
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _validate_signing_placement(request: DocumentRequest, recipients: Sequence[RecipientsInner]) -> None:
|
|
263
|
+
if request.with_signature_page is True or request.text_tags is True:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
field_groups = request.fields or []
|
|
267
|
+
fields = [field for file_fields in field_groups for field in file_fields]
|
|
268
|
+
assigned_recipient_ids = {str(field.recipient_id) for field in fields if getattr(field, "recipient_id", None)}
|
|
269
|
+
missing_recipients = [
|
|
270
|
+
recipient for recipient in recipients if recipient.id and recipient.id not in assigned_recipient_ids
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
if not fields or missing_recipients:
|
|
274
|
+
raise ValueError(
|
|
275
|
+
"Embedded signing documents must include fields for every recipient, "
|
|
276
|
+
"set with_signature_page=True, or set text_tags=True"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _recipient_attrs(recipient: Mapping[str, Any], index: int) -> dict[str, Any]:
|
|
281
|
+
attrs = {
|
|
282
|
+
"id": _mapping_value(recipient, "id") or str(index + 1),
|
|
283
|
+
"name": _mapping_value(recipient, "name"),
|
|
284
|
+
"email": _mapping_value(recipient, "email"),
|
|
285
|
+
}
|
|
286
|
+
passcode = _optional_string(_mapping_value(recipient, "passcode"))
|
|
287
|
+
if passcode:
|
|
288
|
+
attrs["passcode"] = passcode
|
|
289
|
+
return attrs
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _build_iframe_options(
|
|
293
|
+
*,
|
|
294
|
+
url: str,
|
|
295
|
+
allowed_embed_hosts: Sequence[str] | None,
|
|
296
|
+
allowed_redirect_hosts: Sequence[str] | None,
|
|
297
|
+
values: Mapping[str, str | None],
|
|
298
|
+
booleans: Mapping[str, bool | None],
|
|
299
|
+
redirects: Mapping[str, str | None],
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
options: dict[str, Any] = {"url": _validate_embed_url(url, allowed_embed_hosts)}
|
|
302
|
+
for value_key, string_value in values.items():
|
|
303
|
+
if string_value is not None:
|
|
304
|
+
options[value_key] = string_value
|
|
305
|
+
for boolean_key, boolean_value in booleans.items():
|
|
306
|
+
if boolean_value is not None:
|
|
307
|
+
options[boolean_key] = boolean_value
|
|
308
|
+
for redirect_key, redirect_value in redirects.items():
|
|
309
|
+
if redirect_value is not None:
|
|
310
|
+
options[redirect_key] = _validate_redirect_url(redirect_value, allowed_redirect_hosts)
|
|
311
|
+
return options
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _build_embed_script(options: Mapping[str, Any], events: Mapping[str, str], auto_open: bool) -> str:
|
|
315
|
+
options_json = _html_safe_json(options)
|
|
316
|
+
lines = [f"var signwellEmbed = new SignWellEmbed({options_json});"]
|
|
317
|
+
for event_name, handler_path in events.items():
|
|
318
|
+
if handler_path is None:
|
|
319
|
+
continue
|
|
320
|
+
path = _validate_handler_path(str(handler_path).strip())
|
|
321
|
+
safe_event = _html_safe_json(event_name)
|
|
322
|
+
lines.append(f"signwellEmbed.on({safe_event}, {path});")
|
|
323
|
+
if auto_open:
|
|
324
|
+
lines.append("signwellEmbed.open();")
|
|
325
|
+
body = "\n".join(lines)
|
|
326
|
+
return f"<script>\n{body}\n</script>"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _html_safe_json(value: Any) -> str:
|
|
330
|
+
return (
|
|
331
|
+
json.dumps(value, separators=(",", ":")).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _validate_embed_url(url: str, allowed_hosts: Sequence[str] | None) -> str:
|
|
336
|
+
parsed = _parse_https_url(url)
|
|
337
|
+
host = (parsed.hostname or "").lower()
|
|
338
|
+
allowed = {allowed_host.lower() for allowed_host in allowed_hosts or ()}
|
|
339
|
+
if host not in DEFAULT_SIGNWELL_HOSTS and not host.endswith(".signwell.com") and host not in allowed:
|
|
340
|
+
raise ValueError("Embedded URLs must be SignWell-hosted unless explicitly allowed")
|
|
341
|
+
return url
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _validate_redirect_url(url: str, allowed_hosts: Sequence[str] | None) -> str:
|
|
345
|
+
parsed = _parse_https_url(url)
|
|
346
|
+
if allowed_hosts is None:
|
|
347
|
+
raise ValueError("allowed_redirect_hosts is required when passing redirect URLs")
|
|
348
|
+
allowed = {allowed_host.lower() for allowed_host in allowed_hosts}
|
|
349
|
+
if (parsed.hostname or "").lower() not in allowed:
|
|
350
|
+
raise ValueError("Redirect URL host is not allowed")
|
|
351
|
+
return url
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _parse_https_url(url: str):
|
|
355
|
+
parsed = urlparse(url)
|
|
356
|
+
if parsed.scheme != "https":
|
|
357
|
+
raise ValueError("URL must use HTTPS")
|
|
358
|
+
if not parsed.hostname:
|
|
359
|
+
raise ValueError("URL must include a host")
|
|
360
|
+
if parsed.username or parsed.password:
|
|
361
|
+
raise ValueError("URL must not include credentials")
|
|
362
|
+
return parsed
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _validate_handler_path(path: str) -> str:
|
|
366
|
+
if not SAFE_HANDLER_PATH.fullmatch(path):
|
|
367
|
+
raise ValueError("Event handler must be a dot-separated JavaScript identifier path")
|
|
368
|
+
if any(segment in BLOCKED_HANDLER_SEGMENTS for segment in path.split(".")):
|
|
369
|
+
raise ValueError("Event handler path contains a blocked segment")
|
|
370
|
+
return path
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _mapping_value(mapping: Mapping[str, Any], key: str) -> Any:
|
|
374
|
+
return mapping.get(key)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _optional_string(value: Any) -> str | None:
|
|
378
|
+
if value is None:
|
|
379
|
+
return None
|
|
380
|
+
normalized = str(value).strip()
|
|
381
|
+
return normalized or None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _present(value: Any) -> bool:
|
|
385
|
+
return value is not None and (not hasattr(value, "__len__") or len(value) > 0)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SignWell Developer API
|
|
3
|
+
|
|
4
|
+
API for creating, managing, and tracking electronic signature workflows.
|
|
5
|
+
|
|
6
|
+
The version of the OpenAPI document: v1
|
|
7
|
+
Contact: support@signwell.com
|
|
8
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
9
|
+
|
|
10
|
+
Do not edit the class manually.
|
|
11
|
+
""" # noqa: E501
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from email.utils import parsedate_to_datetime
|
|
16
|
+
from typing import Any, NoReturn, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class RateLimitInfo:
|
|
21
|
+
limit: float | None = None
|
|
22
|
+
remaining: float | None = None
|
|
23
|
+
reset: float | None = None
|
|
24
|
+
reset_at: datetime | None = None
|
|
25
|
+
retry_after: float | None = None
|
|
26
|
+
retry_after_at: datetime | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenApiException(Exception):
|
|
30
|
+
"""Base exception class for generated SDK errors."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApiTypeError(OpenApiException, TypeError):
|
|
34
|
+
def __init__(self, msg, path_to_item=None, valid_classes=None, key_type=None) -> None:
|
|
35
|
+
self.path_to_item = path_to_item
|
|
36
|
+
self.valid_classes = valid_classes
|
|
37
|
+
self.key_type = key_type
|
|
38
|
+
full_msg = msg
|
|
39
|
+
if path_to_item:
|
|
40
|
+
full_msg = f"{msg} at {render_path(path_to_item)}"
|
|
41
|
+
super().__init__(full_msg)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ApiValueError(OpenApiException, ValueError):
|
|
45
|
+
def __init__(self, msg, path_to_item=None) -> None:
|
|
46
|
+
self.path_to_item = path_to_item
|
|
47
|
+
full_msg = msg
|
|
48
|
+
if path_to_item:
|
|
49
|
+
full_msg = f"{msg} at {render_path(path_to_item)}"
|
|
50
|
+
super().__init__(full_msg)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ApiAttributeError(OpenApiException, AttributeError):
|
|
54
|
+
def __init__(self, msg, path_to_item=None) -> None:
|
|
55
|
+
self.path_to_item = path_to_item
|
|
56
|
+
full_msg = msg
|
|
57
|
+
if path_to_item:
|
|
58
|
+
full_msg = f"{msg} at {render_path(path_to_item)}"
|
|
59
|
+
super().__init__(full_msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ApiKeyError(OpenApiException, KeyError):
|
|
63
|
+
def __init__(self, msg, path_to_item=None) -> None:
|
|
64
|
+
self.path_to_item = path_to_item
|
|
65
|
+
full_msg = msg
|
|
66
|
+
if path_to_item:
|
|
67
|
+
full_msg = f"{msg} at {render_path(path_to_item)}"
|
|
68
|
+
super().__init__(full_msg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ApiException(OpenApiException):
|
|
72
|
+
_SENSITIVE_HEADER_NAMES = {
|
|
73
|
+
"authorization",
|
|
74
|
+
"cookie",
|
|
75
|
+
"proxy-authorization",
|
|
76
|
+
"set-cookie",
|
|
77
|
+
"x-api-key",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
status=None,
|
|
83
|
+
reason=None,
|
|
84
|
+
http_resp=None,
|
|
85
|
+
*,
|
|
86
|
+
body: str | None = None,
|
|
87
|
+
data: Any | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.status = status
|
|
90
|
+
self.reason = reason
|
|
91
|
+
self.body = body
|
|
92
|
+
self.data = data
|
|
93
|
+
self.headers = None
|
|
94
|
+
|
|
95
|
+
if http_resp:
|
|
96
|
+
if self.status is None:
|
|
97
|
+
self.status = http_resp.status
|
|
98
|
+
if self.reason is None:
|
|
99
|
+
self.reason = http_resp.reason
|
|
100
|
+
if self.body is None:
|
|
101
|
+
response_data = getattr(http_resp, "data", None)
|
|
102
|
+
if isinstance(response_data, bytes):
|
|
103
|
+
try:
|
|
104
|
+
self.body = response_data.decode("utf-8")
|
|
105
|
+
except UnicodeDecodeError:
|
|
106
|
+
self.body = None
|
|
107
|
+
elif isinstance(response_data, str):
|
|
108
|
+
self.body = response_data
|
|
109
|
+
self.headers = http_resp.getheaders()
|
|
110
|
+
self.rate_limit = extract_rate_limit_info(self.headers)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def _redacted_headers(cls, headers):
|
|
114
|
+
if headers is None:
|
|
115
|
+
return None
|
|
116
|
+
try:
|
|
117
|
+
items = headers.items()
|
|
118
|
+
except AttributeError:
|
|
119
|
+
try:
|
|
120
|
+
items = list(headers)
|
|
121
|
+
except TypeError:
|
|
122
|
+
return headers
|
|
123
|
+
redacted = {}
|
|
124
|
+
for name, value in items:
|
|
125
|
+
if str(name).lower() in cls._SENSITIVE_HEADER_NAMES:
|
|
126
|
+
redacted[name] = "[REDACTED]"
|
|
127
|
+
else:
|
|
128
|
+
redacted[name] = value
|
|
129
|
+
return redacted
|
|
130
|
+
|
|
131
|
+
def __str__(self):
|
|
132
|
+
error_message = f"({self.status})\nReason: {self.reason}\n"
|
|
133
|
+
redacted_headers = self._redacted_headers(self.headers)
|
|
134
|
+
if redacted_headers:
|
|
135
|
+
error_message += f"HTTP response headers: {redacted_headers}\n"
|
|
136
|
+
if self.data or self.body:
|
|
137
|
+
error_message += "HTTP response body/data omitted; inspect exc.body or exc.data explicitly.\n"
|
|
138
|
+
return error_message
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_response(
|
|
142
|
+
cls,
|
|
143
|
+
*,
|
|
144
|
+
http_resp,
|
|
145
|
+
body: str | None,
|
|
146
|
+
data: Any | None,
|
|
147
|
+
) -> NoReturn:
|
|
148
|
+
status = http_resp.status
|
|
149
|
+
if status == 400:
|
|
150
|
+
raise BadRequestError(http_resp=http_resp, body=body, data=data)
|
|
151
|
+
if status == 401:
|
|
152
|
+
raise AuthenticationError(http_resp=http_resp, body=body, data=data)
|
|
153
|
+
if status == 403:
|
|
154
|
+
raise PermissionDeniedError(http_resp=http_resp, body=body, data=data)
|
|
155
|
+
if status == 404:
|
|
156
|
+
raise NotFoundError(http_resp=http_resp, body=body, data=data)
|
|
157
|
+
if status == 409:
|
|
158
|
+
raise ConflictError(http_resp=http_resp, body=body, data=data)
|
|
159
|
+
if status == 422:
|
|
160
|
+
raise UnprocessableEntityError(http_resp=http_resp, body=body, data=data)
|
|
161
|
+
if status == 429:
|
|
162
|
+
raise RateLimitError(http_resp=http_resp, body=body, data=data)
|
|
163
|
+
if 500 <= status <= 599:
|
|
164
|
+
raise InternalServerError(http_resp=http_resp, body=body, data=data)
|
|
165
|
+
raise ApiException(http_resp=http_resp, body=body, data=data)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ApiError(ApiException):
|
|
169
|
+
"""Base class for SignWell API response errors."""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class BadRequestError(ApiError):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AuthenticationError(ApiError):
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PermissionDeniedError(ApiError):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class NotFoundError(ApiError):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ConflictError(ApiError):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class UnprocessableEntityError(ApiError):
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class RateLimitError(ApiError):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class InternalServerError(ApiError):
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ApiConnectionError(ApiException):
|
|
205
|
+
"""Raised for network transport failures before a response is available."""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class ApiTimeoutError(ApiConnectionError):
|
|
209
|
+
"""Raised when urllib3 times out before receiving a complete response."""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class FetchError(ApiConnectionError):
|
|
213
|
+
"""Compatibility class for fetch-style transport failures."""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class RequiredError(ApiValueError):
|
|
217
|
+
"""Raised when a required API parameter is missing."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, field: str, message: str | None = None) -> None:
|
|
220
|
+
self.field = field
|
|
221
|
+
super().__init__(message or f'Required parameter "{field}" was null or undefined.')
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class UnsupportedContentTypeError(ApiException):
|
|
225
|
+
"""Raised when a JSON model response uses an unsupported content type."""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Backward-compatible SignWell aliases.
|
|
229
|
+
ForbiddenError = PermissionDeniedError
|
|
230
|
+
TransportError = ApiConnectionError
|
|
231
|
+
|
|
232
|
+
# OpenAPI Generator compatibility aliases.
|
|
233
|
+
BadRequestException = BadRequestError
|
|
234
|
+
UnauthorizedException = AuthenticationError
|
|
235
|
+
ForbiddenException = ForbiddenError
|
|
236
|
+
NotFoundException = NotFoundError
|
|
237
|
+
ConflictException = ConflictError
|
|
238
|
+
UnprocessableEntityException = UnprocessableEntityError
|
|
239
|
+
ServiceException = InternalServerError
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def extract_rate_limit_info(headers: Any) -> RateLimitInfo | None:
|
|
243
|
+
limit = _parse_number(_first_header(headers, ["x-ratelimit-limit", "ratelimit-limit"]))
|
|
244
|
+
remaining = _parse_number(_first_header(headers, ["x-ratelimit-remaining", "ratelimit-remaining"]))
|
|
245
|
+
reset = _parse_number(_first_header(headers, ["x-ratelimit-reset", "ratelimit-reset"]))
|
|
246
|
+
retry_after_value = _first_header(headers, ["retry-after"])
|
|
247
|
+
retry_after = _parse_number(retry_after_value)
|
|
248
|
+
retry_after_at = None if retry_after is not None else _parse_datetime(retry_after_value)
|
|
249
|
+
reset_at = datetime.fromtimestamp(reset, tz=timezone.utc) if reset is not None and reset > 1_000_000_000 else None
|
|
250
|
+
|
|
251
|
+
if all(value is None for value in (limit, remaining, reset, retry_after, retry_after_at)):
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
return RateLimitInfo(
|
|
255
|
+
limit=limit,
|
|
256
|
+
remaining=remaining,
|
|
257
|
+
reset=reset,
|
|
258
|
+
reset_at=reset_at,
|
|
259
|
+
retry_after=retry_after,
|
|
260
|
+
retry_after_at=retry_after_at,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _first_header(headers: Any, names: list[str]) -> str | None:
|
|
265
|
+
if headers is None:
|
|
266
|
+
return None
|
|
267
|
+
try:
|
|
268
|
+
items = headers.items()
|
|
269
|
+
except AttributeError:
|
|
270
|
+
try:
|
|
271
|
+
items = list(headers)
|
|
272
|
+
except TypeError:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
lowered = {name.lower() for name in names}
|
|
276
|
+
for name, value in items:
|
|
277
|
+
if str(name).lower() not in lowered:
|
|
278
|
+
continue
|
|
279
|
+
values = value if isinstance(value, list | tuple) else [value]
|
|
280
|
+
for item in values:
|
|
281
|
+
if isinstance(item, str) and item.strip():
|
|
282
|
+
return item
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _parse_number(value: str | None) -> float | None:
|
|
287
|
+
if value is None:
|
|
288
|
+
return None
|
|
289
|
+
token = value.strip().split(",", maxsplit=1)[0].split(";", maxsplit=1)[0].strip()
|
|
290
|
+
try:
|
|
291
|
+
return float(token)
|
|
292
|
+
except ValueError:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _parse_datetime(value: str | None) -> datetime | None:
|
|
297
|
+
if value is None:
|
|
298
|
+
return None
|
|
299
|
+
try:
|
|
300
|
+
parsed = parsedate_to_datetime(value)
|
|
301
|
+
except (TypeError, ValueError):
|
|
302
|
+
return None
|
|
303
|
+
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=timezone.utc)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def render_path(path_to_item):
|
|
307
|
+
"""Returns a string representation of a path."""
|
|
308
|
+
result = ""
|
|
309
|
+
for pth in path_to_item:
|
|
310
|
+
if isinstance(pth, int):
|
|
311
|
+
result += f"[{pth}]"
|
|
312
|
+
else:
|
|
313
|
+
result += f"['{pth}']"
|
|
314
|
+
return result
|