easycontract 0.1.4__tar.gz → 0.1.7__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.
- {easycontract-0.1.4 → easycontract-0.1.7}/CHANGELOG.md +78 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/PKG-INFO +1 -1
- {easycontract-0.1.4 → easycontract-0.1.7}/pyproject.toml +1 -1
- {easycontract-0.1.4 → easycontract-0.1.7}/src/easycontract/__init__.py +1 -1
- {easycontract-0.1.4 → easycontract-0.1.7}/src/easycontract/client.py +2 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/src/easycontract/resources.py +526 -3
- {easycontract-0.1.4 → easycontract-0.1.7}/.gitignore +0 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/LICENSE.txt +0 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/README.md +0 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/src/easycontract/errors.py +0 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/src/easycontract/webhooks.py +0 -0
- {easycontract-0.1.4 → easycontract-0.1.7}/tests/smoke_test.py +0 -0
|
@@ -2,6 +2,84 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `easycontract` Python SDK.
|
|
4
4
|
|
|
5
|
+
## 0.1.7 (2026-11-10)
|
|
6
|
+
|
|
7
|
+
**Features**
|
|
8
|
+
|
|
9
|
+
- Template versioning: envelopes are created from the latest APPROVED
|
|
10
|
+
version of a template, never from its draft.
|
|
11
|
+
`client.templates.list_versions/approve_version/get_version`;
|
|
12
|
+
`create_envelope` on a template with no approved version raises the
|
|
13
|
+
`template_not_approved` error (409). Templates and envelopes carry the
|
|
14
|
+
version (`current_version`, `envelope["template"]["version"]`).
|
|
15
|
+
- `client.templates.update_role` (a new key rewrites the role's tokens in
|
|
16
|
+
the documents; `adopt_tokens_of` gives a role the tokens of a key no role
|
|
17
|
+
has, listed in the template's `unknown_roles` with `expand=roles`) and
|
|
18
|
+
`client.templates.delete_role`.
|
|
19
|
+
- `client.templates.set_margin_image` / `delete_margin_image`: a logo or
|
|
20
|
+
seal in the header or footer of an authored document (PNG, JPEG or WebP,
|
|
21
|
+
2 MB at most; `align` start, center or end). A new image is checked at
|
|
22
|
+
once (400 if refused), then waits for the malware scan before it is
|
|
23
|
+
published: the template document carries `header_image` /
|
|
24
|
+
`footer_image` (the published one: type, size, SHA-256, place) and
|
|
25
|
+
`header_image_upload` / `footer_image_upload` (one still in the scan, or
|
|
26
|
+
refused).
|
|
27
|
+
- `client.templates.preview(technical=True)`: the technical view of the
|
|
28
|
+
template draft (every fillable field explained, highlighted in the
|
|
29
|
+
documents with example values and how the API passes each one).
|
|
30
|
+
- `client.templates.compare_versions` (what changed between two versions,
|
|
31
|
+
or a version and the draft) and `client.templates.restore_version` (copy
|
|
32
|
+
an earlier version into the draft; approving it makes a new version).
|
|
33
|
+
- `client.organization.group()`: the organization group (Enterprise,
|
|
34
|
+
read only), its members and the invitations received; `features` gains
|
|
35
|
+
`organization_groups`. `client.organization.group_templates()` and
|
|
36
|
+
`copy_group_template()`: templates the other organizations of the group
|
|
37
|
+
offer, copied as a draft of your own; `templates.update(group_shared=...)`
|
|
38
|
+
offers one, and templates carry `group_shared` and `copied_from`.
|
|
39
|
+
A copy follows its source: `client.templates.upstream()` (newer approved
|
|
40
|
+
version?), `compare_upstream()` and `pull_upstream()` (into the draft);
|
|
41
|
+
`templates.update(follow_updates=...)` switches the e-mail notices.
|
|
42
|
+
- Attention points in other languages: `client.templates.list_attention_translations`,
|
|
43
|
+
`get_attention_translation`, `set_attention_translation` (drafts),
|
|
44
|
+
`delete_attention_translation` and `autotranslate_attention_points` (a
|
|
45
|
+
machine draft; a document's autotranslation drafts them too). Template
|
|
46
|
+
points carry `translations` (status per language), translation jobs
|
|
47
|
+
carry `scope`, and the version comparison `attention_point_translations`.
|
|
48
|
+
A template is usable in a language only when its attention points are
|
|
49
|
+
approved in it too (approval stays in the panel).
|
|
50
|
+
- `templates.request_countersign_authorization(scope=...)`: `"version"`
|
|
51
|
+
(default) or `"future_versions"` (the authorization also covers the
|
|
52
|
+
versions approved later). Authorizations carry `template_version` and
|
|
53
|
+
`scope`.
|
|
54
|
+
|
|
55
|
+
## 0.1.6 (2026-11-08)
|
|
56
|
+
|
|
57
|
+
**Features**
|
|
58
|
+
|
|
59
|
+
- Attention points: parts of the contract the signer must notice, shown on
|
|
60
|
+
the signing page before the documents (never stamped in the PDF).
|
|
61
|
+
`client.templates.list_attention_points/add_attention_point/
|
|
62
|
+
update_attention_point/delete_attention_point` (the defaults of every
|
|
63
|
+
envelope created from the template) and the same four on
|
|
64
|
+
`client.envelopes` (draft envelopes; on one created from a template, Pro
|
|
65
|
+
and Enterprise). `requires_ack=True` makes the signer confirm it before
|
|
66
|
+
signing. `templates.create_envelope(attention_points=[…])` replaces the
|
|
67
|
+
template's for that envelope only (Pro and Enterprise).
|
|
68
|
+
|
|
69
|
+
## 0.1.5 (2026-10-23)
|
|
70
|
+
|
|
71
|
+
**Features**
|
|
72
|
+
|
|
73
|
+
- Automatic countersignature of the organization (Pro+):
|
|
74
|
+
`client.templates.request_countersign_authorization(template_id,
|
|
75
|
+
authorizer_email=…, authorizer_title=…, valid_until="YYYY-MM-DD",
|
|
76
|
+
max_uses=None, variables={…})` (the authorizer signs it by e-mail),
|
|
77
|
+
`client.templates.list_countersign_authorizations`,
|
|
78
|
+
`client.countersign_authorizations.list/get/revoke`,
|
|
79
|
+
`client.envelopes.retry_countersign` and
|
|
80
|
+
`client.envelopes.skip_countersign(reason=…)`. `templates.create_envelope`
|
|
81
|
+
accepts `countersign="none"`; envelopes carry `countersign`.
|
|
82
|
+
|
|
5
83
|
## 0.1.4 (2026-10-20)
|
|
6
84
|
|
|
7
85
|
**Features**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easycontract
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: SDK oficial de Python para el API de easycontract (firma electrónica eIDAS)
|
|
5
5
|
Project-URL: Documentation, https://easycontract.online/docs/api
|
|
6
6
|
Author-email: Román Ramírez <roman@easycontract.online>
|
|
@@ -23,6 +23,7 @@ import requests
|
|
|
23
23
|
from . import __version__
|
|
24
24
|
from .errors import APIError, error_from_response
|
|
25
25
|
from .resources import (
|
|
26
|
+
CountersignAuthorizations,
|
|
26
27
|
Envelopes,
|
|
27
28
|
Events,
|
|
28
29
|
Identity,
|
|
@@ -109,6 +110,7 @@ class EasyContract:
|
|
|
109
110
|
self.identity = Identity(self)
|
|
110
111
|
self.retention = Retention(self)
|
|
111
112
|
self.templates = Templates(self)
|
|
113
|
+
self.countersign_authorizations = CountersignAuthorizations(self)
|
|
112
114
|
self.webhooks = Webhooks(self)
|
|
113
115
|
self.projects = Projects(self)
|
|
114
116
|
self.events = Events(self)
|
|
@@ -14,6 +14,15 @@ from urllib.parse import quote
|
|
|
14
14
|
from .errors import EasyContractError
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _attention_body(title, body, document_id, page, requires_ack) -> dict:
|
|
18
|
+
payload: dict = {"title": title, "body": body, "requires_ack": requires_ack}
|
|
19
|
+
if document_id is not None:
|
|
20
|
+
payload["document_id"] = document_id
|
|
21
|
+
if page is not None:
|
|
22
|
+
payload["page"] = page
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
|
|
17
26
|
def _id(value) -> str:
|
|
18
27
|
"""A resource identifier as a path segment: it must be a UUID (every
|
|
19
28
|
resource id of the API is). Anything else — "../webhooks/x", "a?b",
|
|
@@ -38,6 +47,12 @@ def _seg(value) -> str:
|
|
|
38
47
|
OPERATIONS: dict[tuple[str, str], str] = {
|
|
39
48
|
("GET", "/whoami/"): "client.whoami",
|
|
40
49
|
("GET", "/organization/"): "client.organization.get",
|
|
50
|
+
("GET", "/organization/group/"): "client.organization.group",
|
|
51
|
+
("GET", "/organization/group/templates/"): "client.organization.group_templates",
|
|
52
|
+
(
|
|
53
|
+
"POST",
|
|
54
|
+
"/organization/group/templates/{template_id}/copy/",
|
|
55
|
+
): "client.organization.copy_group_template",
|
|
41
56
|
("GET", "/members/"): "client.members.list",
|
|
42
57
|
("PATCH", "/members/{membership_id}/"): "client.members.change_role",
|
|
43
58
|
("DELETE", "/members/{membership_id}/"): "client.members.remove",
|
|
@@ -59,6 +74,31 @@ OPERATIONS: dict[tuple[str, str], str] = {
|
|
|
59
74
|
"/envelopes/{envelope_id}/signers/{signer_id}/embedded-session/",
|
|
60
75
|
): "client.envelopes.create_embedded_session",
|
|
61
76
|
("POST", "/envelopes/{envelope_id}/void/"): "client.envelopes.void",
|
|
77
|
+
(
|
|
78
|
+
"POST",
|
|
79
|
+
"/envelopes/{envelope_id}/countersign/retry/",
|
|
80
|
+
): "client.envelopes.retry_countersign",
|
|
81
|
+
(
|
|
82
|
+
"POST",
|
|
83
|
+
"/envelopes/{envelope_id}/countersign/skip/",
|
|
84
|
+
): "client.envelopes.skip_countersign",
|
|
85
|
+
("GET", "/countersign-authorizations/"): "client.countersign_authorizations.list",
|
|
86
|
+
(
|
|
87
|
+
"GET",
|
|
88
|
+
"/countersign-authorizations/{authorization_id}/",
|
|
89
|
+
): "client.countersign_authorizations.get",
|
|
90
|
+
(
|
|
91
|
+
"POST",
|
|
92
|
+
"/countersign-authorizations/{authorization_id}/revoke/",
|
|
93
|
+
): "client.countersign_authorizations.revoke",
|
|
94
|
+
(
|
|
95
|
+
"GET",
|
|
96
|
+
"/templates/{template_id}/countersign-authorizations/",
|
|
97
|
+
): "client.templates.list_countersign_authorizations",
|
|
98
|
+
(
|
|
99
|
+
"POST",
|
|
100
|
+
"/templates/{template_id}/countersign-authorizations/",
|
|
101
|
+
): "client.templates.request_countersign_authorization",
|
|
62
102
|
("GET", "/envelopes/{envelope_id}/certificate/"): "client.envelopes.download_certificate",
|
|
63
103
|
(
|
|
64
104
|
"POST",
|
|
@@ -141,9 +181,85 @@ OPERATIONS: dict[tuple[str, str], str] = {
|
|
|
141
181
|
): "client.templates.autotranslate",
|
|
142
182
|
("GET", "/translation-jobs/{job_id}/"): "client.templates.get_translation_job",
|
|
143
183
|
("POST", "/templates/{template_id}/preview/"): "client.templates.preview",
|
|
184
|
+
(
|
|
185
|
+
"POST",
|
|
186
|
+
"/templates/{template_id}/documents/{document_id}/margin-images/{slot}/",
|
|
187
|
+
): "client.templates.set_margin_image",
|
|
188
|
+
(
|
|
189
|
+
"DELETE",
|
|
190
|
+
"/templates/{template_id}/documents/{document_id}/margin-images/{slot}/",
|
|
191
|
+
): "client.templates.delete_margin_image",
|
|
144
192
|
("POST", "/templates/{template_id}/duplicate/"): "client.templates.duplicate",
|
|
145
193
|
("POST", "/templates/{template_id}/roles/"): "client.templates.add_role",
|
|
194
|
+
("PATCH", "/templates/{template_id}/roles/{role_id}/"): "client.templates.update_role",
|
|
195
|
+
("DELETE", "/templates/{template_id}/roles/{role_id}/"): "client.templates.delete_role",
|
|
196
|
+
("GET", "/templates/{template_id}/versions/"): "client.templates.list_versions",
|
|
197
|
+
("POST", "/templates/{template_id}/versions/"): "client.templates.approve_version",
|
|
198
|
+
("GET", "/templates/{template_id}/versions/{number}/"): "client.templates.get_version",
|
|
199
|
+
("GET", "/templates/{template_id}/versions/compare/"): "client.templates.compare_versions",
|
|
200
|
+
(
|
|
201
|
+
"POST",
|
|
202
|
+
"/templates/{template_id}/versions/{number}/restore/",
|
|
203
|
+
): "client.templates.restore_version",
|
|
146
204
|
("POST", "/templates/{template_id}/fields/"): "client.templates.add_field",
|
|
205
|
+
("GET", "/templates/{template_id}/upstream/"): "client.templates.upstream",
|
|
206
|
+
(
|
|
207
|
+
"GET",
|
|
208
|
+
"/templates/{template_id}/upstream/compare/",
|
|
209
|
+
): "client.templates.compare_upstream",
|
|
210
|
+
("POST", "/templates/{template_id}/upstream/pull/"): "client.templates.pull_upstream",
|
|
211
|
+
(
|
|
212
|
+
"GET",
|
|
213
|
+
"/templates/{template_id}/attention-points/",
|
|
214
|
+
): "client.templates.list_attention_points",
|
|
215
|
+
(
|
|
216
|
+
"POST",
|
|
217
|
+
"/templates/{template_id}/attention-points/",
|
|
218
|
+
): "client.templates.add_attention_point",
|
|
219
|
+
(
|
|
220
|
+
"PATCH",
|
|
221
|
+
"/templates/{template_id}/attention-points/{point_id}/",
|
|
222
|
+
): "client.templates.update_attention_point",
|
|
223
|
+
(
|
|
224
|
+
"DELETE",
|
|
225
|
+
"/templates/{template_id}/attention-points/{point_id}/",
|
|
226
|
+
): "client.templates.delete_attention_point",
|
|
227
|
+
(
|
|
228
|
+
"GET",
|
|
229
|
+
"/templates/{template_id}/attention-points/translations/",
|
|
230
|
+
): "client.templates.list_attention_translations",
|
|
231
|
+
(
|
|
232
|
+
"GET",
|
|
233
|
+
"/templates/{template_id}/attention-points/translations/{language}/",
|
|
234
|
+
): "client.templates.get_attention_translation",
|
|
235
|
+
(
|
|
236
|
+
"PUT",
|
|
237
|
+
"/templates/{template_id}/attention-points/translations/{language}/",
|
|
238
|
+
): "client.templates.set_attention_translation",
|
|
239
|
+
(
|
|
240
|
+
"DELETE",
|
|
241
|
+
"/templates/{template_id}/attention-points/translations/{language}/",
|
|
242
|
+
): "client.templates.delete_attention_translation",
|
|
243
|
+
(
|
|
244
|
+
"POST",
|
|
245
|
+
"/templates/{template_id}/attention-points/translations/{language}/autotranslate/",
|
|
246
|
+
): "client.templates.autotranslate_attention_points",
|
|
247
|
+
(
|
|
248
|
+
"GET",
|
|
249
|
+
"/envelopes/{envelope_id}/attention-points/",
|
|
250
|
+
): "client.envelopes.list_attention_points",
|
|
251
|
+
(
|
|
252
|
+
"POST",
|
|
253
|
+
"/envelopes/{envelope_id}/attention-points/",
|
|
254
|
+
): "client.envelopes.add_attention_point",
|
|
255
|
+
(
|
|
256
|
+
"PATCH",
|
|
257
|
+
"/envelopes/{envelope_id}/attention-points/{point_id}/",
|
|
258
|
+
): "client.envelopes.update_attention_point",
|
|
259
|
+
(
|
|
260
|
+
"DELETE",
|
|
261
|
+
"/envelopes/{envelope_id}/attention-points/{point_id}/",
|
|
262
|
+
): "client.envelopes.delete_attention_point",
|
|
147
263
|
("POST", "/templates/{template_id}/envelopes/"): "client.templates.create_envelope",
|
|
148
264
|
("GET", "/events/"): "client.events.list",
|
|
149
265
|
("GET", "/webhooks/"): "client.webhooks.list",
|
|
@@ -173,6 +289,36 @@ def _file_tuple(file, name: str | None, content_type: str = "application/pdf"):
|
|
|
173
289
|
return (name or getattr(file, "name", "document.pdf"), file, content_type)
|
|
174
290
|
|
|
175
291
|
|
|
292
|
+
def _image_mime(file, name: str | None) -> str:
|
|
293
|
+
"""Content type of an image upload, from its first bytes when it can be
|
|
294
|
+
read without consuming it, else from the file name (the server decides
|
|
295
|
+
anyway: it decodes and re-encodes every image)."""
|
|
296
|
+
head = b""
|
|
297
|
+
if isinstance(file, bytes):
|
|
298
|
+
head = file[:12]
|
|
299
|
+
elif isinstance(file, (str, Path)):
|
|
300
|
+
with Path(file).open("rb") as fh:
|
|
301
|
+
head = fh.read(12)
|
|
302
|
+
if head.startswith(b"\x89PNG"):
|
|
303
|
+
return "image/png"
|
|
304
|
+
if head.startswith(b"\xff\xd8"):
|
|
305
|
+
return "image/jpeg"
|
|
306
|
+
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
|
307
|
+
return "image/webp"
|
|
308
|
+
if name:
|
|
309
|
+
label = name
|
|
310
|
+
elif isinstance(file, (str, Path)):
|
|
311
|
+
label = str(file)
|
|
312
|
+
else:
|
|
313
|
+
label = str(getattr(file, "name", "") or "")
|
|
314
|
+
lower = label.lower()
|
|
315
|
+
if lower.endswith((".jpg", ".jpeg")):
|
|
316
|
+
return "image/jpeg"
|
|
317
|
+
if lower.endswith(".webp"):
|
|
318
|
+
return "image/webp"
|
|
319
|
+
return "image/png"
|
|
320
|
+
|
|
321
|
+
|
|
176
322
|
class _Resource:
|
|
177
323
|
def __init__(self, client):
|
|
178
324
|
self._client = client
|
|
@@ -372,11 +518,64 @@ class Envelopes(_Resource):
|
|
|
372
518
|
def release_legal_hold(self, envelope_id: str) -> dict:
|
|
373
519
|
return self._client.request("DELETE", f"/envelopes/{_id(envelope_id)}/legal-hold/")
|
|
374
520
|
|
|
521
|
+
def list_attention_points(self, envelope_id: str) -> dict:
|
|
522
|
+
"""Attention points shown to the signers before the documents, with
|
|
523
|
+
who confirmed each one."""
|
|
524
|
+
return self._client.request("GET", f"/envelopes/{_id(envelope_id)}/attention-points/")
|
|
525
|
+
|
|
526
|
+
def add_attention_point(
|
|
527
|
+
self,
|
|
528
|
+
envelope_id: str,
|
|
529
|
+
*,
|
|
530
|
+
title: str,
|
|
531
|
+
body: str = "",
|
|
532
|
+
document_id: str | None = None,
|
|
533
|
+
page: int | None = None,
|
|
534
|
+
requires_ack: bool = False,
|
|
535
|
+
idempotency_key: str | None = None,
|
|
536
|
+
) -> dict:
|
|
537
|
+
"""Draft envelopes only. On an envelope created from a template it is
|
|
538
|
+
Pro and Enterprise (403 feature_not_available otherwise)."""
|
|
539
|
+
return self._client.request(
|
|
540
|
+
"POST",
|
|
541
|
+
f"/envelopes/{_id(envelope_id)}/attention-points/",
|
|
542
|
+
json=_attention_body(title, body, document_id, page, requires_ack),
|
|
543
|
+
idempotency_key=idempotency_key,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def update_attention_point(self, envelope_id: str, point_id: str, **changes) -> dict:
|
|
547
|
+
"""title, body, document_id, page, requires_ack (drafts only)."""
|
|
548
|
+
return self._client.request(
|
|
549
|
+
"PATCH",
|
|
550
|
+
f"/envelopes/{_id(envelope_id)}/attention-points/{_id(point_id)}/",
|
|
551
|
+
json=changes,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def delete_attention_point(self, envelope_id: str, point_id: str):
|
|
555
|
+
return self._client.request(
|
|
556
|
+
"DELETE", f"/envelopes/{_id(envelope_id)}/attention-points/{_id(point_id)}/"
|
|
557
|
+
)
|
|
558
|
+
|
|
375
559
|
def void(self, envelope_id: str, *, reason: str = "") -> dict:
|
|
376
560
|
return self._client.request(
|
|
377
561
|
"POST", f"/envelopes/{_id(envelope_id)}/void/", json={"reason": reason}
|
|
378
562
|
)
|
|
379
563
|
|
|
564
|
+
def retry_countersign(self, envelope_id: str) -> dict:
|
|
565
|
+
"""Signed by everybody and waiting (countersign.status == "failed"):
|
|
566
|
+
try the organization's countersignature again, e.g. after a new
|
|
567
|
+
authorization was signed."""
|
|
568
|
+
return self._client.request("POST", f"/envelopes/{_id(envelope_id)}/countersign/retry/")
|
|
569
|
+
|
|
570
|
+
def skip_countersign(self, envelope_id: str, *, reason: str) -> dict:
|
|
571
|
+
"""Complete it WITHOUT the organization's countersignature (the
|
|
572
|
+
reason is required and stated in the certificate)."""
|
|
573
|
+
return self._client.request(
|
|
574
|
+
"POST",
|
|
575
|
+
f"/envelopes/{_id(envelope_id)}/countersign/skip/",
|
|
576
|
+
json={"reason": reason},
|
|
577
|
+
)
|
|
578
|
+
|
|
380
579
|
def download_certificate(self, envelope_id: str, *, to: str | Path | None = None) -> bytes:
|
|
381
580
|
content = self._client.request(
|
|
382
581
|
"GET", f"/envelopes/{_id(envelope_id)}/certificate/", raw=True
|
|
@@ -475,7 +674,9 @@ class Templates(_Resource):
|
|
|
475
674
|
|
|
476
675
|
def update(self, template_id: str, **changes) -> dict:
|
|
477
676
|
"""PATCH settings: name, description, signing_order,
|
|
478
|
-
retention_period_months, variables (schema list), metadata
|
|
677
|
+
retention_period_months, variables (schema list), metadata,
|
|
678
|
+
group_shared (offer it to the organization group), follow_updates
|
|
679
|
+
(a group copy: notices of the source's new versions)."""
|
|
479
680
|
return self._client.request("PATCH", f"/templates/{_id(template_id)}/", json=changes)
|
|
480
681
|
|
|
481
682
|
def catalog(self, *, language: str | None = None) -> dict:
|
|
@@ -653,6 +854,47 @@ class Templates(_Resource):
|
|
|
653
854
|
data={"name": name} if name else None,
|
|
654
855
|
)
|
|
655
856
|
|
|
857
|
+
def set_margin_image(
|
|
858
|
+
self,
|
|
859
|
+
template_id: str,
|
|
860
|
+
document_id: str,
|
|
861
|
+
slot: str,
|
|
862
|
+
*,
|
|
863
|
+
file=None,
|
|
864
|
+
align: str | None = None,
|
|
865
|
+
name: str | None = None,
|
|
866
|
+
) -> dict:
|
|
867
|
+
"""Put, replace or move the image of the ``slot`` margin (``"header"``
|
|
868
|
+
or ``"footer"``) of an authored document of the template draft: a
|
|
869
|
+
PNG, JPEG or WebP of 2 MB at most (``file``: path, bytes or file
|
|
870
|
+
object) and/or ``align`` (``"start"``, ``"center"``, ``"end"``).
|
|
871
|
+
A new image is published after its malware scan (see
|
|
872
|
+
``header_image_upload`` / ``footer_image_upload`` in the returned
|
|
873
|
+
document); approving a new version then puts it in new envelopes."""
|
|
874
|
+
if slot not in ("header", "footer"):
|
|
875
|
+
raise ValueError("slot must be 'header' or 'footer'")
|
|
876
|
+
path = f"/templates/{_id(template_id)}/documents/{_id(document_id)}/margin-images/{slot}/"
|
|
877
|
+
if file is None:
|
|
878
|
+
if align is None:
|
|
879
|
+
raise ValueError("give file, align or both")
|
|
880
|
+
return self._client.request("POST", path, json={"align": align})
|
|
881
|
+
mime = _image_mime(file, name)
|
|
882
|
+
return self._client.request(
|
|
883
|
+
"POST",
|
|
884
|
+
path,
|
|
885
|
+
files={"file": _file_tuple(file, name or f"{slot}.{mime.split('/')[1]}", mime)},
|
|
886
|
+
data={"align": align} if align else None,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
def delete_margin_image(self, template_id: str, document_id: str, slot: str) -> dict:
|
|
890
|
+
"""Remove the image of the ``slot`` margin (``"header"`` or ``"footer"``)."""
|
|
891
|
+
if slot not in ("header", "footer"):
|
|
892
|
+
raise ValueError("slot must be 'header' or 'footer'")
|
|
893
|
+
return self._client.request(
|
|
894
|
+
"DELETE",
|
|
895
|
+
f"/templates/{_id(template_id)}/documents/{_id(document_id)}/margin-images/{slot}/",
|
|
896
|
+
)
|
|
897
|
+
|
|
656
898
|
def preview(
|
|
657
899
|
self,
|
|
658
900
|
template_id: str,
|
|
@@ -662,11 +904,16 @@ class Templates(_Resource):
|
|
|
662
904
|
signers: dict[str, dict] | None = None,
|
|
663
905
|
title: str = "",
|
|
664
906
|
language: str | None = None,
|
|
907
|
+
technical: bool = False,
|
|
665
908
|
to: str | Path | None = None,
|
|
666
909
|
) -> bytes:
|
|
667
910
|
"""PDF preview of the authored documents (sample values where
|
|
668
|
-
none are given).
|
|
911
|
+
none are given). ``technical=True``: the technical view of the
|
|
912
|
+
draft instead (every fillable field explained, with examples and
|
|
913
|
+
how the API passes each one). Nothing is persisted."""
|
|
669
914
|
body: dict = {"variables": variables or {}, "signers": signers or {}, "title": title}
|
|
915
|
+
if technical:
|
|
916
|
+
body["technical"] = True
|
|
670
917
|
if document_id:
|
|
671
918
|
body["document_id"] = document_id
|
|
672
919
|
if language:
|
|
@@ -719,6 +966,186 @@ class Templates(_Resource):
|
|
|
719
966
|
body["auth_methods"] = auth_methods
|
|
720
967
|
return self._client.request("POST", f"/templates/{_id(template_id)}/roles/", json=body)
|
|
721
968
|
|
|
969
|
+
def update_role(
|
|
970
|
+
self,
|
|
971
|
+
template_id: str,
|
|
972
|
+
role_id: str,
|
|
973
|
+
*,
|
|
974
|
+
key: str | None = None,
|
|
975
|
+
label: str | None = None,
|
|
976
|
+
order: int | None = None,
|
|
977
|
+
auth_methods: list[str] | None = None,
|
|
978
|
+
adopt_tokens_of: str | None = None,
|
|
979
|
+
) -> dict:
|
|
980
|
+
"""Edit a role of the template draft. A new ``key`` rewrites the
|
|
981
|
+
role's tokens in every authored document and translation (changed
|
|
982
|
+
translations go back to draft); ``adopt_tokens_of`` gives the role
|
|
983
|
+
the tokens of a key no role has (``unknown_roles`` of the template
|
|
984
|
+
with ``expand=roles``); ``renamed_in`` says where."""
|
|
985
|
+
body: dict = {}
|
|
986
|
+
for name, value in (
|
|
987
|
+
("key", key),
|
|
988
|
+
("label", label),
|
|
989
|
+
("order", order),
|
|
990
|
+
("auth_methods", auth_methods),
|
|
991
|
+
("adopt_tokens_of", adopt_tokens_of),
|
|
992
|
+
):
|
|
993
|
+
if value is not None:
|
|
994
|
+
body[name] = value
|
|
995
|
+
return self._client.request(
|
|
996
|
+
"PATCH", f"/templates/{_id(template_id)}/roles/{_id(role_id)}/", json=body
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
def delete_role(self, template_id: str, role_id: str) -> None:
|
|
1000
|
+
"""Remove a role from the draft (refused while a document uses its tokens)."""
|
|
1001
|
+
self._client.request("DELETE", f"/templates/{_id(template_id)}/roles/{_id(role_id)}/")
|
|
1002
|
+
|
|
1003
|
+
def list_versions(self, template_id: str) -> dict:
|
|
1004
|
+
"""Approved versions kept, newest first, plus ``current_version`` and
|
|
1005
|
+
``has_unapproved_changes`` (the draft differs from the version in use)."""
|
|
1006
|
+
return self._client.request("GET", f"/templates/{_id(template_id)}/versions/")
|
|
1007
|
+
|
|
1008
|
+
def approve_version(
|
|
1009
|
+
self, template_id: str, *, note: str = "", idempotency_key: str | None = None
|
|
1010
|
+
) -> dict:
|
|
1011
|
+
"""Approve the draft as the next version: envelopes created from now
|
|
1012
|
+
on use it. Raises on 409 ``template_unchanged`` when nothing changed."""
|
|
1013
|
+
return self._client.request(
|
|
1014
|
+
"POST",
|
|
1015
|
+
f"/templates/{_id(template_id)}/versions/",
|
|
1016
|
+
json={"note": note} if note else {},
|
|
1017
|
+
idempotency_key=idempotency_key,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
def compare_versions(
|
|
1021
|
+
self, template_id: str, *, from_: int | str | None = None, to: int | str = "draft"
|
|
1022
|
+
) -> dict:
|
|
1023
|
+
"""What changed from ``from_`` to ``to`` (a version number or
|
|
1024
|
+
``"draft"``). Default: from the version in use to the draft."""
|
|
1025
|
+
params: dict = {"to": str(to)}
|
|
1026
|
+
if from_ is not None:
|
|
1027
|
+
params["from"] = str(from_)
|
|
1028
|
+
return self._client.request(
|
|
1029
|
+
"GET", f"/templates/{_id(template_id)}/versions/compare/", params=params
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
def restore_version(
|
|
1033
|
+
self, template_id: str, number: int, *, idempotency_key: str | None = None
|
|
1034
|
+
) -> dict:
|
|
1035
|
+
"""Copy version ``number`` into the draft (unapproved draft work is
|
|
1036
|
+
lost). Approve the draft afterwards: it becomes a new version."""
|
|
1037
|
+
return self._client.request(
|
|
1038
|
+
"POST",
|
|
1039
|
+
f"/templates/{_id(template_id)}/versions/{int(number)}/restore/",
|
|
1040
|
+
idempotency_key=idempotency_key,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
def get_version(self, template_id: str, number: int) -> dict:
|
|
1044
|
+
return self._client.request("GET", f"/templates/{_id(template_id)}/versions/{int(number)}/")
|
|
1045
|
+
|
|
1046
|
+
def list_attention_points(self, template_id: str) -> dict:
|
|
1047
|
+
return self._client.request("GET", f"/templates/{_id(template_id)}/attention-points/")
|
|
1048
|
+
|
|
1049
|
+
def add_attention_point(
|
|
1050
|
+
self,
|
|
1051
|
+
template_id: str,
|
|
1052
|
+
*,
|
|
1053
|
+
title: str,
|
|
1054
|
+
body: str = "",
|
|
1055
|
+
document_id: str | None = None,
|
|
1056
|
+
page: int | None = None,
|
|
1057
|
+
requires_ack: bool = False,
|
|
1058
|
+
idempotency_key: str | None = None,
|
|
1059
|
+
) -> dict:
|
|
1060
|
+
"""Default attention point of every envelope created from the template."""
|
|
1061
|
+
return self._client.request(
|
|
1062
|
+
"POST",
|
|
1063
|
+
f"/templates/{_id(template_id)}/attention-points/",
|
|
1064
|
+
json=_attention_body(title, body, document_id, page, requires_ack),
|
|
1065
|
+
idempotency_key=idempotency_key,
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
def update_attention_point(self, template_id: str, point_id: str, **changes) -> dict:
|
|
1069
|
+
"""title, body, document_id, page, requires_ack."""
|
|
1070
|
+
return self._client.request(
|
|
1071
|
+
"PATCH",
|
|
1072
|
+
f"/templates/{_id(template_id)}/attention-points/{_id(point_id)}/",
|
|
1073
|
+
json=changes,
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
def delete_attention_point(self, template_id: str, point_id: str):
|
|
1077
|
+
return self._client.request(
|
|
1078
|
+
"DELETE", f"/templates/{_id(template_id)}/attention-points/{_id(point_id)}/"
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
def upstream(self, template_id: str) -> dict:
|
|
1082
|
+
"""A copy of a group template: its source and whether it has a newer
|
|
1083
|
+
approved version (`update_available`)."""
|
|
1084
|
+
return self._client.request("GET", f"/templates/{_id(template_id)}/upstream/")
|
|
1085
|
+
|
|
1086
|
+
def compare_upstream(self, template_id: str) -> dict:
|
|
1087
|
+
"""What pulling the source's latest approved version would change."""
|
|
1088
|
+
return self._client.request("GET", f"/templates/{_id(template_id)}/upstream/compare/")
|
|
1089
|
+
|
|
1090
|
+
def pull_upstream(self, template_id: str, *, idempotency_key: str | None = None) -> dict:
|
|
1091
|
+
"""The source's latest approved version replaces the copy's draft
|
|
1092
|
+
(translations as drafts); approve the draft to use it."""
|
|
1093
|
+
return self._client.request(
|
|
1094
|
+
"POST",
|
|
1095
|
+
f"/templates/{_id(template_id)}/upstream/pull/",
|
|
1096
|
+
json={},
|
|
1097
|
+
idempotency_key=idempotency_key,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
def list_attention_translations(self, template_id: str) -> dict:
|
|
1101
|
+
"""Per language: how many attention points are approved, stale, draft
|
|
1102
|
+
or missing. `complete` = usable for envelopes in that language."""
|
|
1103
|
+
return self._client.request(
|
|
1104
|
+
"GET", f"/templates/{_id(template_id)}/attention-points/translations/"
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
def get_attention_translation(self, template_id: str, language: str) -> dict:
|
|
1108
|
+
"""The attention points in one language, with their source text."""
|
|
1109
|
+
return self._client.request(
|
|
1110
|
+
"GET", f"/templates/{_id(template_id)}/attention-points/translations/{_seg(language)}/"
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
def set_attention_translation(self, template_id: str, language: str, points: list) -> dict:
|
|
1114
|
+
"""Write the text of one or more points in a language, as drafts:
|
|
1115
|
+
`points=[{"point_id": ..., "title": ..., "body": ...}]`. Approval is
|
|
1116
|
+
a human act in the panel."""
|
|
1117
|
+
return self._client.request(
|
|
1118
|
+
"PUT",
|
|
1119
|
+
f"/templates/{_id(template_id)}/attention-points/translations/{_seg(language)}/",
|
|
1120
|
+
json={"points": points},
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
def delete_attention_translation(self, template_id: str, language: str):
|
|
1124
|
+
return self._client.request(
|
|
1125
|
+
"DELETE",
|
|
1126
|
+
f"/templates/{_id(template_id)}/attention-points/translations/{_seg(language)}/",
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
def autotranslate_attention_points(
|
|
1130
|
+
self,
|
|
1131
|
+
template_id: str,
|
|
1132
|
+
language: str,
|
|
1133
|
+
*,
|
|
1134
|
+
acknowledge: bool,
|
|
1135
|
+
overwrite: bool = False,
|
|
1136
|
+
idempotency_key: str | None = None,
|
|
1137
|
+
) -> dict:
|
|
1138
|
+
"""Queue a MACHINE DRAFT of the attention points in `language` (202,
|
|
1139
|
+
poll `get_translation_job`). A document's autotranslation drafts
|
|
1140
|
+
them too. `overwrite` also replaces points with their own or an
|
|
1141
|
+
approved text."""
|
|
1142
|
+
return self._client.request(
|
|
1143
|
+
"POST",
|
|
1144
|
+
f"/templates/{_id(template_id)}/attention-points/translations/{_seg(language)}/autotranslate/",
|
|
1145
|
+
json={"acknowledge": acknowledge, "overwrite": overwrite},
|
|
1146
|
+
idempotency_key=idempotency_key,
|
|
1147
|
+
)
|
|
1148
|
+
|
|
722
1149
|
def add_field(
|
|
723
1150
|
self,
|
|
724
1151
|
template_id: str,
|
|
@@ -768,12 +1195,19 @@ class Templates(_Resource):
|
|
|
768
1195
|
kyc_images_retention: str | None = None,
|
|
769
1196
|
biometrics: bool = False,
|
|
770
1197
|
project: str | None = None,
|
|
1198
|
+
countersign: str | None = None,
|
|
1199
|
+
attention_points: list[dict] | None = None,
|
|
771
1200
|
idempotency_key: str | None = None,
|
|
772
1201
|
) -> dict:
|
|
773
1202
|
"""The one-call automation flow (§10.4): instantiate and optionally
|
|
774
1203
|
send, getting each signer's sign_url back for embedded flows.
|
|
775
1204
|
`variables` fills the tokens of authored documents; `language` picks
|
|
776
|
-
the source or an approved translation.""
|
|
1205
|
+
the source or an approved translation. `countersign="none"` creates it
|
|
1206
|
+
without the organization's automatic countersignature (default
|
|
1207
|
+
"auto": countersigned when the template has an active authorization;
|
|
1208
|
+
other values than the authorized ones give 409). `attention_points`
|
|
1209
|
+
replaces the template's for this envelope only (Pro and Enterprise;
|
|
1210
|
+
None = the template's, [] = none)."""
|
|
777
1211
|
body: dict = {
|
|
778
1212
|
"signers": signers,
|
|
779
1213
|
"title": title,
|
|
@@ -792,6 +1226,10 @@ class Templates(_Resource):
|
|
|
792
1226
|
body["expires_in_days"] = expires_in_days
|
|
793
1227
|
if project:
|
|
794
1228
|
body["project"] = project
|
|
1229
|
+
if countersign:
|
|
1230
|
+
body["countersign"] = countersign
|
|
1231
|
+
if attention_points is not None:
|
|
1232
|
+
body["attention_points"] = attention_points
|
|
795
1233
|
return self._client.request(
|
|
796
1234
|
"POST",
|
|
797
1235
|
f"/templates/{_id(template_id)}/envelopes/",
|
|
@@ -799,6 +1237,69 @@ class Templates(_Resource):
|
|
|
799
1237
|
idempotency_key=idempotency_key,
|
|
800
1238
|
)
|
|
801
1239
|
|
|
1240
|
+
def request_countersign_authorization(
|
|
1241
|
+
self,
|
|
1242
|
+
template_id: str,
|
|
1243
|
+
*,
|
|
1244
|
+
authorizer_email: str,
|
|
1245
|
+
authorizer_title: str,
|
|
1246
|
+
valid_until: str,
|
|
1247
|
+
max_uses: int | None = None,
|
|
1248
|
+
variables: dict | None = None,
|
|
1249
|
+
project: str | None = None,
|
|
1250
|
+
scope: str | None = None,
|
|
1251
|
+
) -> dict:
|
|
1252
|
+
"""Ask an owner/admin to sign the standing authorization for the
|
|
1253
|
+
organization's automatic countersignature (Pro+). The signing link
|
|
1254
|
+
goes to the authorizer by e-mail only; it becomes active when signed.
|
|
1255
|
+
``scope``: ``"version"`` (default, the approved version in use only) or
|
|
1256
|
+
``"future_versions"`` (also the versions approved later, except one
|
|
1257
|
+
adding variables whose values it does not fix)."""
|
|
1258
|
+
body: dict = {
|
|
1259
|
+
"authorizer_email": authorizer_email,
|
|
1260
|
+
"authorizer_title": authorizer_title,
|
|
1261
|
+
"valid_until": valid_until,
|
|
1262
|
+
"variables": variables or {},
|
|
1263
|
+
}
|
|
1264
|
+
if max_uses is not None:
|
|
1265
|
+
body["max_uses"] = max_uses
|
|
1266
|
+
if project:
|
|
1267
|
+
body["project"] = project
|
|
1268
|
+
if scope:
|
|
1269
|
+
body["scope"] = scope
|
|
1270
|
+
return self._client.request(
|
|
1271
|
+
"POST", f"/templates/{_id(template_id)}/countersign-authorizations/", json=body
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
def list_countersign_authorizations(
|
|
1275
|
+
self, template_id: str, *, limit: int | None = None, cursor: str | None = None
|
|
1276
|
+
) -> dict:
|
|
1277
|
+
params = {k: v for k, v in {"limit": limit, "cursor": cursor}.items() if v is not None}
|
|
1278
|
+
return self._client.request(
|
|
1279
|
+
"GET", f"/templates/{_id(template_id)}/countersign-authorizations/", params=params
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
class CountersignAuthorizations(_Resource):
|
|
1284
|
+
"""Standing authorizations for the organization's automatic
|
|
1285
|
+
countersignature (docs: «Contrafirma automática»)."""
|
|
1286
|
+
|
|
1287
|
+
def list(self, *, limit: int | None = None, cursor: str | None = None) -> dict:
|
|
1288
|
+
params = {k: v for k, v in {"limit": limit, "cursor": cursor}.items() if v is not None}
|
|
1289
|
+
return self._client.request("GET", "/countersign-authorizations/", params=params)
|
|
1290
|
+
|
|
1291
|
+
def get(self, authorization_id: str) -> dict:
|
|
1292
|
+
return self._client.request("GET", f"/countersign-authorizations/{_id(authorization_id)}/")
|
|
1293
|
+
|
|
1294
|
+
def revoke(self, authorization_id: str, *, reason: str = "") -> dict:
|
|
1295
|
+
"""Revoke an active one (what was countersigned stays) or cancel a
|
|
1296
|
+
pending one (its signing envelope is voided)."""
|
|
1297
|
+
return self._client.request(
|
|
1298
|
+
"POST",
|
|
1299
|
+
f"/countersign-authorizations/{_id(authorization_id)}/revoke/",
|
|
1300
|
+
json={"reason": reason},
|
|
1301
|
+
)
|
|
1302
|
+
|
|
802
1303
|
|
|
803
1304
|
class Webhooks(_Resource):
|
|
804
1305
|
def create(
|
|
@@ -1029,6 +1530,28 @@ class Organization(_Resource):
|
|
|
1029
1530
|
"""Org info including plan limits and features."""
|
|
1030
1531
|
return self._client.request("GET", "/organization/")
|
|
1031
1532
|
|
|
1533
|
+
def group(self) -> dict:
|
|
1534
|
+
"""The organization group (Enterprise): the group, its members and the
|
|
1535
|
+
invitations received. Read only: the acts happen in the panel."""
|
|
1536
|
+
return self._client.request("GET", "/organization/group/")
|
|
1537
|
+
|
|
1538
|
+
def group_templates(self) -> dict:
|
|
1539
|
+
"""Templates the other organizations of the group offer (approved
|
|
1540
|
+
version, names only)."""
|
|
1541
|
+
return self._client.request("GET", "/organization/group/templates/")
|
|
1542
|
+
|
|
1543
|
+
def copy_group_template(
|
|
1544
|
+
self, template_id: str, *, name: str = "", idempotency_key: str | None = None
|
|
1545
|
+
) -> dict:
|
|
1546
|
+
"""Copy an offered template into this organization as a new DRAFT
|
|
1547
|
+
(translations too): approve it and request its own countersign."""
|
|
1548
|
+
return self._client.request(
|
|
1549
|
+
"POST",
|
|
1550
|
+
f"/organization/group/templates/{_id(template_id)}/copy/",
|
|
1551
|
+
json={"name": name} if name else {},
|
|
1552
|
+
idempotency_key=idempotency_key,
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1032
1555
|
def update(
|
|
1033
1556
|
self,
|
|
1034
1557
|
*,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|