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.
Files changed (110) hide show
  1. signwell_sdk/__init__.py +287 -0
  2. signwell_sdk/api/__init__.py +10 -0
  3. signwell_sdk/api/api_application_api.py +481 -0
  4. signwell_sdk/api/bulk_send_api.py +1589 -0
  5. signwell_sdk/api/document_api.py +2425 -0
  6. signwell_sdk/api/me_api.py +246 -0
  7. signwell_sdk/api/regional_api.py +345 -0
  8. signwell_sdk/api/template_api.py +1298 -0
  9. signwell_sdk/api/webhooks_api.py +710 -0
  10. signwell_sdk/api_client.py +747 -0
  11. signwell_sdk/api_response.py +20 -0
  12. signwell_sdk/configuration.py +599 -0
  13. signwell_sdk/embedded.py +385 -0
  14. signwell_sdk/exceptions.py +314 -0
  15. signwell_sdk/models/__init__.py +114 -0
  16. signwell_sdk/models/account_info_response.py +143 -0
  17. signwell_sdk/models/account_info_response_active_users_inner.py +107 -0
  18. signwell_sdk/models/account_info_response_preferences.py +147 -0
  19. signwell_sdk/models/additional_fields_inner_inner.py +274 -0
  20. signwell_sdk/models/additional_fields_inner_inner_value.py +189 -0
  21. signwell_sdk/models/additional_files_inner.py +103 -0
  22. signwell_sdk/models/api_application_response.py +137 -0
  23. signwell_sdk/models/api_application_response_owner.py +107 -0
  24. signwell_sdk/models/api_application_response_preferences.py +135 -0
  25. signwell_sdk/models/attachment_request_info.py +99 -0
  26. signwell_sdk/models/attachment_requests_inner.py +110 -0
  27. signwell_sdk/models/bulk_send_create_response.py +132 -0
  28. signwell_sdk/models/bulk_send_csv_request.py +100 -0
  29. signwell_sdk/models/bulk_send_csv_template_response.py +97 -0
  30. signwell_sdk/models/bulk_send_documents_response.py +167 -0
  31. signwell_sdk/models/bulk_send_list_item.py +141 -0
  32. signwell_sdk/models/bulk_send_list_response.py +138 -0
  33. signwell_sdk/models/bulk_send_response.py +151 -0
  34. signwell_sdk/models/bulk_send_response_templates_inner.py +98 -0
  35. signwell_sdk/models/bulk_send_validate_csv_response.py +129 -0
  36. signwell_sdk/models/checkbox_group_info.py +138 -0
  37. signwell_sdk/models/checkbox_groups_inner.py +136 -0
  38. signwell_sdk/models/checkbox_validation.py +35 -0
  39. signwell_sdk/models/completed_pdf_response.py +177 -0
  40. signwell_sdk/models/completed_pdf_url_response.py +97 -0
  41. signwell_sdk/models/copied_contact_info.py +99 -0
  42. signwell_sdk/models/copied_contacts_inner.py +98 -0
  43. signwell_sdk/models/copied_placeholders_inner.py +111 -0
  44. signwell_sdk/models/create_bulk_send_request.py +155 -0
  45. signwell_sdk/models/create_webhook_request.py +100 -0
  46. signwell_sdk/models/date_format.py +36 -0
  47. signwell_sdk/models/detailed_option.py +105 -0
  48. signwell_sdk/models/document_from_template_request.py +377 -0
  49. signwell_sdk/models/document_from_template_response.py +353 -0
  50. signwell_sdk/models/document_from_template_response_fields_inner_inner.py +270 -0
  51. signwell_sdk/models/document_from_template_response_fields_inner_inner_recipient.py +99 -0
  52. signwell_sdk/models/document_from_template_response_recipients_inner.py +206 -0
  53. signwell_sdk/models/document_list_response.py +138 -0
  54. signwell_sdk/models/document_request.py +350 -0
  55. signwell_sdk/models/document_response.py +357 -0
  56. signwell_sdk/models/document_response_attachment_requests_inner.py +101 -0
  57. signwell_sdk/models/document_response_fields_inner_inner.py +268 -0
  58. signwell_sdk/models/document_response_fields_inner_inner_options_inner.py +101 -0
  59. signwell_sdk/models/document_response_fields_inner_inner_recipient.py +98 -0
  60. signwell_sdk/models/document_response_fields_inner_inner_value.py +195 -0
  61. signwell_sdk/models/document_response_recipients_inner.py +208 -0
  62. signwell_sdk/models/document_template_list_response.py +138 -0
  63. signwell_sdk/models/document_template_request.py +307 -0
  64. signwell_sdk/models/document_template_response.py +323 -0
  65. signwell_sdk/models/document_template_response_copied_placeholders_inner.py +131 -0
  66. signwell_sdk/models/document_template_response_placeholders_inner.py +144 -0
  67. signwell_sdk/models/document_template_update_request.py +208 -0
  68. signwell_sdk/models/dropdown_option.py +153 -0
  69. signwell_sdk/models/error_response.py +107 -0
  70. signwell_sdk/models/error_response_meta.py +101 -0
  71. signwell_sdk/models/field_type.py +46 -0
  72. signwell_sdk/models/fields_inner_inner.py +268 -0
  73. signwell_sdk/models/file_format.py +33 -0
  74. signwell_sdk/models/file_info.py +98 -0
  75. signwell_sdk/models/files_inner.py +103 -0
  76. signwell_sdk/models/label_info.py +98 -0
  77. signwell_sdk/models/label_request.py +97 -0
  78. signwell_sdk/models/label_response.py +98 -0
  79. signwell_sdk/models/me_response.py +130 -0
  80. signwell_sdk/models/me_response_contact.py +147 -0
  81. signwell_sdk/models/me_response_user.py +117 -0
  82. signwell_sdk/models/me_response_user_preferences.py +105 -0
  83. signwell_sdk/models/nom151_certificate_response.py +107 -0
  84. signwell_sdk/models/nom151_certificate_response_nom151.py +125 -0
  85. signwell_sdk/models/nom151_url_response.py +97 -0
  86. signwell_sdk/models/pagination_meta.py +119 -0
  87. signwell_sdk/models/placeholders_inner.py +115 -0
  88. signwell_sdk/models/rate_limit_error_response.py +97 -0
  89. signwell_sdk/models/recipients_inner.py +141 -0
  90. signwell_sdk/models/reminder_recipients_inner.py +100 -0
  91. signwell_sdk/models/send_reminder_request.py +114 -0
  92. signwell_sdk/models/template_attachment_requests_inner.py +110 -0
  93. signwell_sdk/models/template_checkbox_groups_inner.py +136 -0
  94. signwell_sdk/models/template_field_values_inner.py +109 -0
  95. signwell_sdk/models/template_field_values_inner_value.py +189 -0
  96. signwell_sdk/models/template_fields_inner_inner.py +266 -0
  97. signwell_sdk/models/template_recipients_inner.py +146 -0
  98. signwell_sdk/models/text_validation.py +42 -0
  99. signwell_sdk/models/update_document_and_send_request.py +234 -0
  100. signwell_sdk/models/update_recipients_map_inner.py +118 -0
  101. signwell_sdk/models/update_recipients_request.py +111 -0
  102. signwell_sdk/models/validation_error_response.py +99 -0
  103. signwell_sdk/models/webhook_response.py +110 -0
  104. signwell_sdk/py.typed +0 -0
  105. signwell_sdk/rest.py +209 -0
  106. signwell_sdk/webhook.py +214 -0
  107. signwell_sdk_python-0.1.0.dist-info/METADATA +177 -0
  108. signwell_sdk_python-0.1.0.dist-info/RECORD +110 -0
  109. signwell_sdk_python-0.1.0.dist-info/WHEEL +4 -0
  110. signwell_sdk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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