blueink-client-python 1.0.0__tar.gz → 1.0.1__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.
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/PKG-INFO +53 -3
- blueink-client-python-1.0.0/src/blueink_client_python.egg-info/PKG-INFO → blueink_client_python-1.0.1/README.md +50 -27
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/setup.cfg +9 -5
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/bundle_helper.py +183 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/client.py +14 -4
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/endpoints.py +6 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/bundles.py +112 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/request_helper.py +9 -1
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/bundle.py +72 -0
- blueink_client_python-1.0.1/src/blueink/subclients/envelope_template.py +64 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/person.py +1 -1
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/template.py +0 -3
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/webhook.py +0 -3
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_bundle_helper.py +130 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_client.py +26 -0
- blueink-client-python-1.0.0/README.md → blueink_client_python-1.0.1/src/blueink_client_python.egg-info/PKG-INFO +77 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/SOURCES.txt +1 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/requires.txt +3 -3
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/LICENSE +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/pyproject.toml +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/__init__.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/constants.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/__init__.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/persons.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/webhook.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/paginator.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/person_helper.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/__init__.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/packet.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/subclient.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/__init__.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_person_helper.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/utils/__init__.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/utils/testcase.py +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/dependency_links.txt +0 -0
- {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: blueink-client-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python Client for Blueink eSignature API
|
|
5
5
|
Home-page: https://github.com/blueinkhq/blueink-client-python
|
|
6
6
|
Author: Blueink
|
|
@@ -19,11 +19,11 @@ Requires-Dist: email-validator
|
|
|
19
19
|
Provides-Extra: munch
|
|
20
20
|
Requires-Dist: munch>=2.5; extra == "munch"
|
|
21
21
|
Provides-Extra: requests
|
|
22
|
-
Requires-Dist: requests>=2.
|
|
22
|
+
Requires-Dist: requests>=2.31; extra == "requests"
|
|
23
23
|
Provides-Extra: pydantic
|
|
24
24
|
Requires-Dist: pydantic>=1.9; extra == "pydantic"
|
|
25
25
|
Provides-Extra: email-validator
|
|
26
|
-
Requires-Dist: 1.2; extra == "email-validator"
|
|
26
|
+
Requires-Dist: email-validator>=1.2; extra == "email-validator"
|
|
27
27
|
|
|
28
28
|
# blueink-client-python
|
|
29
29
|

|
|
@@ -400,6 +400,56 @@ with open("/path/to/file/example.pdf", 'rb') as file:
|
|
|
400
400
|
doc04_key = bh.add_document_by_file(file)
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
+
#### Auto-Placement Fields
|
|
404
|
+
|
|
405
|
+
Auto-placement fields allow you to automatically search for text in documents and place
|
|
406
|
+
signature/input fields at those locations with optional offsets. This eliminates the need
|
|
407
|
+
to manually specify exact coordinates for fields.
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from blueink import BundleHelper, Client
|
|
411
|
+
|
|
412
|
+
bh = BundleHelper(
|
|
413
|
+
label="Auto-Placement Example",
|
|
414
|
+
email_subject="Please sign",
|
|
415
|
+
is_test=True,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
signer_key = bh.add_signer(name="John Doe", email="john@example.com")
|
|
419
|
+
doc_key = bh.add_document_by_url("https://www.irs.gov/pub/irs-pdf/fw9.pdf")
|
|
420
|
+
|
|
421
|
+
# Add auto-placement field that searches for "Signature" text
|
|
422
|
+
bh.add_auto_placement(
|
|
423
|
+
document_key=doc_key,
|
|
424
|
+
kind="sig", # Field type: signature
|
|
425
|
+
search="Signature", # Text to search for
|
|
426
|
+
w=20, # Width
|
|
427
|
+
h=5, # Height
|
|
428
|
+
offset_x=-5, # Move 5 units left from found text
|
|
429
|
+
offset_y=2, # Move 2 units down from found text
|
|
430
|
+
editors=[signer_key],
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Add auto-placement for an input field
|
|
434
|
+
bh.add_auto_placement(
|
|
435
|
+
document_key=doc_key,
|
|
436
|
+
kind="inp", # Field type: input
|
|
437
|
+
search="Address", # Text to search for
|
|
438
|
+
w=20,
|
|
439
|
+
h=2,
|
|
440
|
+
offset_x=8, # Move 8 units right from found text
|
|
441
|
+
editors=[signer_key],
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
client = Client()
|
|
445
|
+
response = client.bundles.create_from_bundle_helper(bh)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Key benefits of auto-placement:**
|
|
449
|
+
- No need to manually find exact coordinates
|
|
450
|
+
- Works with template documents that have consistent text labels
|
|
451
|
+
- Automatically adjusts to text position in the document
|
|
452
|
+
- Can be combined with regular manually-positioned fields
|
|
403
453
|
|
|
404
454
|
#### Retrieval
|
|
405
455
|
|
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: blueink-client-python
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: Python Client for Blueink eSignature API
|
|
5
|
-
Home-page: https://github.com/blueinkhq/blueink-client-python
|
|
6
|
-
Author: Blueink
|
|
7
|
-
Author-email: pypi@blueink.com
|
|
8
|
-
Project-URL: Bug Tracker, https://github.com/blueinkhq/blueink-client-python/issues
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.8
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Requires-Dist: munch
|
|
16
|
-
Requires-Dist: requests
|
|
17
|
-
Requires-Dist: pydantic
|
|
18
|
-
Requires-Dist: email-validator
|
|
19
|
-
Provides-Extra: munch
|
|
20
|
-
Requires-Dist: munch>=2.5; extra == "munch"
|
|
21
|
-
Provides-Extra: requests
|
|
22
|
-
Requires-Dist: requests>=2.27; extra == "requests"
|
|
23
|
-
Provides-Extra: pydantic
|
|
24
|
-
Requires-Dist: pydantic>=1.9; extra == "pydantic"
|
|
25
|
-
Provides-Extra: email-validator
|
|
26
|
-
Requires-Dist: 1.2; extra == "email-validator"
|
|
27
|
-
|
|
28
1
|
# blueink-client-python
|
|
29
2
|

|
|
30
3
|

|
|
@@ -400,6 +373,56 @@ with open("/path/to/file/example.pdf", 'rb') as file:
|
|
|
400
373
|
doc04_key = bh.add_document_by_file(file)
|
|
401
374
|
```
|
|
402
375
|
|
|
376
|
+
#### Auto-Placement Fields
|
|
377
|
+
|
|
378
|
+
Auto-placement fields allow you to automatically search for text in documents and place
|
|
379
|
+
signature/input fields at those locations with optional offsets. This eliminates the need
|
|
380
|
+
to manually specify exact coordinates for fields.
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
from blueink import BundleHelper, Client
|
|
384
|
+
|
|
385
|
+
bh = BundleHelper(
|
|
386
|
+
label="Auto-Placement Example",
|
|
387
|
+
email_subject="Please sign",
|
|
388
|
+
is_test=True,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
signer_key = bh.add_signer(name="John Doe", email="john@example.com")
|
|
392
|
+
doc_key = bh.add_document_by_url("https://www.irs.gov/pub/irs-pdf/fw9.pdf")
|
|
393
|
+
|
|
394
|
+
# Add auto-placement field that searches for "Signature" text
|
|
395
|
+
bh.add_auto_placement(
|
|
396
|
+
document_key=doc_key,
|
|
397
|
+
kind="sig", # Field type: signature
|
|
398
|
+
search="Signature", # Text to search for
|
|
399
|
+
w=20, # Width
|
|
400
|
+
h=5, # Height
|
|
401
|
+
offset_x=-5, # Move 5 units left from found text
|
|
402
|
+
offset_y=2, # Move 2 units down from found text
|
|
403
|
+
editors=[signer_key],
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Add auto-placement for an input field
|
|
407
|
+
bh.add_auto_placement(
|
|
408
|
+
document_key=doc_key,
|
|
409
|
+
kind="inp", # Field type: input
|
|
410
|
+
search="Address", # Text to search for
|
|
411
|
+
w=20,
|
|
412
|
+
h=2,
|
|
413
|
+
offset_x=8, # Move 8 units right from found text
|
|
414
|
+
editors=[signer_key],
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
client = Client()
|
|
418
|
+
response = client.bundles.create_from_bundle_helper(bh)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Key benefits of auto-placement:**
|
|
422
|
+
- No need to manually find exact coordinates
|
|
423
|
+
- Works with template documents that have consistent text labels
|
|
424
|
+
- Automatically adjusts to text position in the document
|
|
425
|
+
- Can be combined with regular manually-positioned fields
|
|
403
426
|
|
|
404
427
|
#### Retrieval
|
|
405
428
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = blueink-client-python
|
|
3
|
-
version = 1.0.
|
|
3
|
+
version = 1.0.1
|
|
4
4
|
author = Blueink
|
|
5
5
|
author_email = pypi@blueink.com
|
|
6
6
|
description = Python Client for Blueink eSignature API
|
|
@@ -26,10 +26,14 @@ install_requires =
|
|
|
26
26
|
email-validator
|
|
27
27
|
|
|
28
28
|
[options.extras_require]
|
|
29
|
-
munch =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
munch =
|
|
30
|
+
munch>=2.5
|
|
31
|
+
requests =
|
|
32
|
+
requests>=2.31
|
|
33
|
+
pydantic =
|
|
34
|
+
pydantic>=1.9
|
|
35
|
+
email-validator =
|
|
36
|
+
email-validator>=1.2
|
|
33
37
|
|
|
34
38
|
[options.packages.find]
|
|
35
39
|
where = src
|
|
@@ -4,8 +4,11 @@ from os.path import basename
|
|
|
4
4
|
from typing import List
|
|
5
5
|
|
|
6
6
|
from blueink.model.bundles import (
|
|
7
|
+
AutoPlacement,
|
|
7
8
|
Bundle,
|
|
8
9
|
Document,
|
|
10
|
+
EnvelopeTemplate,
|
|
11
|
+
EnvelopeTemplateFieldValue,
|
|
9
12
|
Field,
|
|
10
13
|
Packet,
|
|
11
14
|
TemplateRef,
|
|
@@ -49,6 +52,7 @@ class BundleHelper:
|
|
|
49
52
|
self._packets = {}
|
|
50
53
|
self._custom_key = custom_key
|
|
51
54
|
self._team = team
|
|
55
|
+
self._envelope_template = None
|
|
52
56
|
|
|
53
57
|
# for file uploads, index should match those in the document "file_index" field
|
|
54
58
|
self.file_names = []
|
|
@@ -252,6 +256,72 @@ class BundleHelper:
|
|
|
252
256
|
self._documents[document_key].add_field(field)
|
|
253
257
|
return field.key
|
|
254
258
|
|
|
259
|
+
def add_auto_placement(
|
|
260
|
+
self,
|
|
261
|
+
document_key: str,
|
|
262
|
+
kind: str,
|
|
263
|
+
search: str,
|
|
264
|
+
w: int,
|
|
265
|
+
h: int,
|
|
266
|
+
offset_x: int = 0,
|
|
267
|
+
offset_y: int = 0,
|
|
268
|
+
editors: List[str] = None,
|
|
269
|
+
page: int = None,
|
|
270
|
+
**additional_data,
|
|
271
|
+
):
|
|
272
|
+
"""Add an auto-placement field to a document.
|
|
273
|
+
|
|
274
|
+
Auto-placement fields automatically search for text in the document and place
|
|
275
|
+
the field at the found location with optional offsets.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
document_key: Key of the document to add the auto-placement to
|
|
279
|
+
kind: Field type (e.g., 'sig' for signature, 'inp' for input, 'ini' for initials)
|
|
280
|
+
search: Text to search for in the document
|
|
281
|
+
w: Width of the field
|
|
282
|
+
h: Height of the field
|
|
283
|
+
offset_x: Horizontal offset from the search text (default: 0)
|
|
284
|
+
offset_y: Vertical offset from the search text (default: 0)
|
|
285
|
+
editors: List of signer keys who can edit this field
|
|
286
|
+
page: Optional page number to limit search to
|
|
287
|
+
additional_data: Optional additional kwargs to append to the auto-placement
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
None (auto-placements don't have keys like regular fields)
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
# Add a signature field that searches for "Signature" text
|
|
294
|
+
bh.add_auto_placement(
|
|
295
|
+
document_key=doc_key,
|
|
296
|
+
kind='sig',
|
|
297
|
+
search='Signature',
|
|
298
|
+
w=20,
|
|
299
|
+
h=5,
|
|
300
|
+
offset_x=-5,
|
|
301
|
+
offset_y=2,
|
|
302
|
+
editors=['signer-1']
|
|
303
|
+
)
|
|
304
|
+
"""
|
|
305
|
+
if document_key not in self._documents:
|
|
306
|
+
raise RuntimeError(f"No document found with key {document_key}!")
|
|
307
|
+
|
|
308
|
+
auto_placement = AutoPlacement.create(
|
|
309
|
+
kind=kind,
|
|
310
|
+
search=search,
|
|
311
|
+
w=w,
|
|
312
|
+
h=h,
|
|
313
|
+
offset_x=offset_x,
|
|
314
|
+
offset_y=offset_y,
|
|
315
|
+
page=page,
|
|
316
|
+
**additional_data,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if editors:
|
|
320
|
+
for editor_key in editors:
|
|
321
|
+
auto_placement.add_editor(editor_key)
|
|
322
|
+
|
|
323
|
+
self._documents[document_key].add_auto_placement(auto_placement)
|
|
324
|
+
|
|
255
325
|
def add_signer(
|
|
256
326
|
self,
|
|
257
327
|
name: str,
|
|
@@ -347,6 +417,64 @@ class BundleHelper:
|
|
|
347
417
|
field_val = TemplateRefFieldValue.create(key, value, **additional_data)
|
|
348
418
|
self._documents[document_key].field_values.append(field_val)
|
|
349
419
|
|
|
420
|
+
def set_envelope_template(
|
|
421
|
+
self, template_id: str, field_values: dict = None, **additional_data
|
|
422
|
+
):
|
|
423
|
+
"""Set the envelope template for this bundle.
|
|
424
|
+
|
|
425
|
+
When using an envelope template, the template contains the complete envelope
|
|
426
|
+
configuration including documents, signers, and field assignments.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
template_id: Envelope template ID (format: T-xxxxxxxxxxx)
|
|
430
|
+
field_values: Optional dict mapping field keys to initial values
|
|
431
|
+
additional_data: Optional additional kwargs to append to the envelope template
|
|
432
|
+
|
|
433
|
+
Example:
|
|
434
|
+
bh = BundleHelper(label="Contract", is_test=True)
|
|
435
|
+
bh.add_signer(name="John Doe", email="john@example.com", key="signer-1")
|
|
436
|
+
bh.set_envelope_template(
|
|
437
|
+
template_id="T-abc123",
|
|
438
|
+
field_values={"company_name": "ACME Corp"}
|
|
439
|
+
)
|
|
440
|
+
"""
|
|
441
|
+
vals = []
|
|
442
|
+
if field_values:
|
|
443
|
+
for field_key, init_val in field_values.items():
|
|
444
|
+
fieldval = EnvelopeTemplateFieldValue.create(
|
|
445
|
+
key=field_key, initial_value=init_val
|
|
446
|
+
)
|
|
447
|
+
vals.append(fieldval)
|
|
448
|
+
|
|
449
|
+
self._envelope_template = EnvelopeTemplate.create(
|
|
450
|
+
template_id=template_id,
|
|
451
|
+
field_values=vals if vals else None,
|
|
452
|
+
**additional_data,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def add_envelope_template_field_value(
|
|
456
|
+
self, key: str, initial_value: str, **additional_data
|
|
457
|
+
):
|
|
458
|
+
"""Add a field value to the envelope template.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
key: Field key in the envelope template
|
|
462
|
+
initial_value: Initial value for the field
|
|
463
|
+
additional_data: Optional additional kwargs
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
RuntimeError: If no envelope template has been set
|
|
467
|
+
"""
|
|
468
|
+
if self._envelope_template is None:
|
|
469
|
+
raise RuntimeError(
|
|
470
|
+
"No envelope template set. Call set_envelope_template() first."
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
field_val = EnvelopeTemplateFieldValue.create(
|
|
474
|
+
key=key, initial_value=initial_value, **additional_data
|
|
475
|
+
)
|
|
476
|
+
self._envelope_template.add_field_value(field_val)
|
|
477
|
+
|
|
350
478
|
def _compile_bundle(self, **additional_data) -> Bundle:
|
|
351
479
|
"""
|
|
352
480
|
Builds a Bundle object complete with all the packets (signers) and documents added through the course
|
|
@@ -384,6 +512,61 @@ class BundleHelper:
|
|
|
384
512
|
bundle = self._compile_bundle(**additional_data)
|
|
385
513
|
return bundle.dict(exclude_unset=True, exclude_none=True)
|
|
386
514
|
|
|
515
|
+
def as_data_for_envelope_template(self, **additional_data):
|
|
516
|
+
"""Return data for creating a bundle from an envelope template.
|
|
517
|
+
|
|
518
|
+
This method is used when creating a bundle from an envelope template.
|
|
519
|
+
It returns a dictionary with packets and envelope_template fields.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
additional_data: extra data to append to the request, as a dict
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Dictionary suitable for create_from_envelope_template endpoint
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
RuntimeError: If no envelope template has been set
|
|
529
|
+
|
|
530
|
+
Example:
|
|
531
|
+
bh = BundleHelper(label="Contract", is_test=True)
|
|
532
|
+
bh.add_signer(name="John Doe", email="john@example.com", key="signer-1")
|
|
533
|
+
bh.set_envelope_template("T-abc123", {"company_name": "ACME"})
|
|
534
|
+
data = bh.as_data_for_envelope_template()
|
|
535
|
+
"""
|
|
536
|
+
if self._envelope_template is None:
|
|
537
|
+
raise RuntimeError(
|
|
538
|
+
"No envelope template set. Call set_envelope_template() first."
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
packets = list(self._packets.values())
|
|
542
|
+
result = {
|
|
543
|
+
"packets": [p.dict(exclude_unset=True, exclude_none=True) for p in packets],
|
|
544
|
+
"envelope_template": self._envelope_template.dict(
|
|
545
|
+
exclude_unset=True, exclude_none=True
|
|
546
|
+
),
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# Add optional bundle fields
|
|
550
|
+
if self._label:
|
|
551
|
+
result["label"] = self._label
|
|
552
|
+
if self._is_test is not None:
|
|
553
|
+
result["is_test"] = self._is_test
|
|
554
|
+
if self._email_subj:
|
|
555
|
+
result["email_subject"] = self._email_subj
|
|
556
|
+
if self._email_msg:
|
|
557
|
+
result["email_message"] = self._email_msg
|
|
558
|
+
if self._cc_emails:
|
|
559
|
+
result["cc_emails"] = self._cc_emails
|
|
560
|
+
if self._custom_key:
|
|
561
|
+
result["custom_key"] = self._custom_key
|
|
562
|
+
if self._team:
|
|
563
|
+
result["team"] = self._team
|
|
564
|
+
|
|
565
|
+
# Add any additional data
|
|
566
|
+
result.update(additional_data)
|
|
567
|
+
|
|
568
|
+
return result
|
|
569
|
+
|
|
387
570
|
def as_json(self, **additional_data):
|
|
388
571
|
"""Return a Bundle as a json
|
|
389
572
|
|
|
@@ -7,6 +7,7 @@ from blueink.constants import (
|
|
|
7
7
|
)
|
|
8
8
|
from blueink.request_helper import RequestHelper
|
|
9
9
|
from blueink.subclients.bundle import BundleSubClient
|
|
10
|
+
from blueink.subclients.envelope_template import EnvelopeTemplateSubClient
|
|
10
11
|
from blueink.subclients.packet import PacketSubClient
|
|
11
12
|
from blueink.subclients.person import PersonSubClient
|
|
12
13
|
from blueink.subclients.template import TemplateSubClient
|
|
@@ -19,6 +20,7 @@ class Client:
|
|
|
19
20
|
private_api_key: str = None,
|
|
20
21
|
base_url: str = None,
|
|
21
22
|
raise_exceptions: bool = True,
|
|
23
|
+
security_headers: dict = None,
|
|
22
24
|
):
|
|
23
25
|
"""Initialize a Client instance to access the Blueink eSignature API
|
|
24
26
|
|
|
@@ -29,6 +31,7 @@ class Client:
|
|
|
29
31
|
base_url: override the API base URL. If not supplied, we check the
|
|
30
32
|
environment variable BLUEINK_API_URL. If that is empty, the default
|
|
31
33
|
value of "https://api.blueink.com/api/v2" is used.
|
|
34
|
+
security_headers: Place for additional security headers, likely unnecessary
|
|
32
35
|
raise_exceptions (Default True): raise HTTPError if code != 200. Otherwise
|
|
33
36
|
return as NormalizedResponse objects.
|
|
34
37
|
|
|
@@ -44,9 +47,9 @@ class Client:
|
|
|
44
47
|
|
|
45
48
|
if not private_api_key:
|
|
46
49
|
raise ValueError(
|
|
47
|
-
"A Blueink Private API Key must be provided on Client initialization"
|
|
48
|
-
" or specified via the environment variable"
|
|
49
|
-
" {ENV_BLUEINK_PRIVATE_API_KEY}"
|
|
50
|
+
f"A Blueink Private API Key must be provided on Client initialization"
|
|
51
|
+
f" or specified via the environment variable"
|
|
52
|
+
f" {ENV_BLUEINK_PRIVATE_API_KEY}"
|
|
50
53
|
)
|
|
51
54
|
|
|
52
55
|
if not base_url:
|
|
@@ -57,10 +60,17 @@ class Client:
|
|
|
57
60
|
|
|
58
61
|
self._base_url = base_url
|
|
59
62
|
|
|
60
|
-
self._request_helper = RequestHelper(
|
|
63
|
+
self._request_helper = RequestHelper(
|
|
64
|
+
private_api_key,
|
|
65
|
+
raise_exceptions,
|
|
66
|
+
security_headers=security_headers,
|
|
67
|
+
)
|
|
61
68
|
|
|
62
69
|
self.bundles = BundleSubClient(self._base_url, self._request_helper)
|
|
63
70
|
self.persons = PersonSubClient(self._base_url, self._request_helper)
|
|
64
71
|
self.packets = PacketSubClient(self._base_url, self._request_helper)
|
|
65
72
|
self.templates = TemplateSubClient(self._base_url, self._request_helper)
|
|
73
|
+
self.envelope_templates = EnvelopeTemplateSubClient(
|
|
74
|
+
self._base_url, self._request_helper
|
|
75
|
+
)
|
|
66
76
|
self.webhooks = WebhookSubClient(self._base_url, self._request_helper)
|
|
@@ -8,6 +8,7 @@ from string import Template
|
|
|
8
8
|
|
|
9
9
|
class BUNDLES:
|
|
10
10
|
CREATE = "/bundles/"
|
|
11
|
+
CREATE_FROM_ENVELOPE_TEMPLATE = "/bundles/create_from_envelope_template/"
|
|
11
12
|
LIST = "/bundles/"
|
|
12
13
|
RETRIEVE = "/bundles/${bundle_id}/"
|
|
13
14
|
CANCEL = "/bundles/${bundle_id}/cancel/"
|
|
@@ -36,6 +37,11 @@ class TEMPLATES:
|
|
|
36
37
|
RETRIEVE = "/templates/${template_id}/"
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
class ENVELOPE_TEMPLATES:
|
|
41
|
+
LIST = "/envelope-templates/"
|
|
42
|
+
RETRIEVE = "/envelope-templates/${envelope_template_id}/"
|
|
43
|
+
|
|
44
|
+
|
|
39
45
|
class WEBHOOKS:
|
|
40
46
|
CREATE = "/webhooks/"
|
|
41
47
|
LIST = "/webhooks/"
|
|
@@ -17,6 +17,70 @@ def generate_key(type, length=5):
|
|
|
17
17
|
return f"{type}_{slug}"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class AutoPlacement(BaseModel):
|
|
21
|
+
"""Model for auto-placement fields that automatically find and place fields on documents"""
|
|
22
|
+
|
|
23
|
+
kind: str = ...
|
|
24
|
+
search: str = ...
|
|
25
|
+
w: int = ...
|
|
26
|
+
h: int = ...
|
|
27
|
+
offset_x: Optional[int] = 0
|
|
28
|
+
offset_y: Optional[int] = 0
|
|
29
|
+
editors: Optional[List[str]]
|
|
30
|
+
page: Optional[int]
|
|
31
|
+
|
|
32
|
+
class Config:
|
|
33
|
+
extra = "allow"
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def create(
|
|
37
|
+
cls,
|
|
38
|
+
kind: str,
|
|
39
|
+
search: str,
|
|
40
|
+
w: int,
|
|
41
|
+
h: int,
|
|
42
|
+
offset_x: int = 0,
|
|
43
|
+
offset_y: int = 0,
|
|
44
|
+
**kwargs,
|
|
45
|
+
):
|
|
46
|
+
"""Create an AutoPlacement instance
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
kind: Field type (e.g., 'sig', 'inp', 'ini', etc.)
|
|
50
|
+
search: Text to search for in the document
|
|
51
|
+
w: Width of the field
|
|
52
|
+
h: Height of the field
|
|
53
|
+
offset_x: Horizontal offset from the search text (default: 0)
|
|
54
|
+
offset_y: Vertical offset from the search text (default: 0)
|
|
55
|
+
**kwargs: Additional parameters like editors, page, etc.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
AutoPlacement instance
|
|
59
|
+
"""
|
|
60
|
+
obj = AutoPlacement(
|
|
61
|
+
kind=kind,
|
|
62
|
+
search=search,
|
|
63
|
+
w=w,
|
|
64
|
+
h=h,
|
|
65
|
+
offset_x=offset_x,
|
|
66
|
+
offset_y=offset_y,
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
return obj
|
|
70
|
+
|
|
71
|
+
@validator("kind")
|
|
72
|
+
def kind_is_allowed(cls, v):
|
|
73
|
+
assert (
|
|
74
|
+
v in FIELD_KIND.values()
|
|
75
|
+
), f"AutoPlacement Kind '{v}' not allowed. Must be one of {FIELD_KIND.values()}"
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
def add_editor(self, editor: str):
|
|
79
|
+
if self.editors is None:
|
|
80
|
+
self.editors = []
|
|
81
|
+
self.editors.append(editor)
|
|
82
|
+
|
|
83
|
+
|
|
20
84
|
class Field(BaseModel):
|
|
21
85
|
kind: str = ...
|
|
22
86
|
key: str = ...
|
|
@@ -112,6 +176,43 @@ class TemplateRefFieldValue(BaseModel):
|
|
|
112
176
|
return obj
|
|
113
177
|
|
|
114
178
|
|
|
179
|
+
class EnvelopeTemplateFieldValue(BaseModel):
|
|
180
|
+
"""Model for field values in envelope templates"""
|
|
181
|
+
|
|
182
|
+
key: str = ...
|
|
183
|
+
initial_value: str = ...
|
|
184
|
+
|
|
185
|
+
class Config:
|
|
186
|
+
extra = "allow"
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def create(cls, key, initial_value, **kwargs):
|
|
190
|
+
obj = EnvelopeTemplateFieldValue(key=key, initial_value=initial_value, **kwargs)
|
|
191
|
+
return obj
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class EnvelopeTemplate(BaseModel):
|
|
195
|
+
"""Model for envelope template reference"""
|
|
196
|
+
|
|
197
|
+
template_id: str = ...
|
|
198
|
+
field_values: Optional[List[EnvelopeTemplateFieldValue]]
|
|
199
|
+
|
|
200
|
+
class Config:
|
|
201
|
+
extra = "allow"
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def create(cls, template_id, field_values=None, **kwargs):
|
|
205
|
+
obj = EnvelopeTemplate(
|
|
206
|
+
template_id=template_id, field_values=field_values, **kwargs
|
|
207
|
+
)
|
|
208
|
+
return obj
|
|
209
|
+
|
|
210
|
+
def add_field_value(self, field_value: EnvelopeTemplateFieldValue):
|
|
211
|
+
if self.field_values is None:
|
|
212
|
+
self.field_values = []
|
|
213
|
+
self.field_values.append(field_value)
|
|
214
|
+
|
|
215
|
+
|
|
115
216
|
class TemplateRef(BaseModel):
|
|
116
217
|
template_id: Optional[str]
|
|
117
218
|
assignments: Optional[List[TemplateRefAssignment]]
|
|
@@ -147,6 +248,7 @@ class Document(BaseModel):
|
|
|
147
248
|
file_b64: Optional[str]
|
|
148
249
|
file_index: Optional[int]
|
|
149
250
|
fields: Optional[List[Field]]
|
|
251
|
+
auto_placements: Optional[List[AutoPlacement]]
|
|
150
252
|
|
|
151
253
|
class Config:
|
|
152
254
|
extra = "allow"
|
|
@@ -163,6 +265,16 @@ class Document(BaseModel):
|
|
|
163
265
|
self.fields = []
|
|
164
266
|
self.fields.append(field)
|
|
165
267
|
|
|
268
|
+
def add_auto_placement(self, auto_placement: AutoPlacement):
|
|
269
|
+
"""Add an auto-placement to this document
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
auto_placement: AutoPlacement instance to add
|
|
273
|
+
"""
|
|
274
|
+
if self.auto_placements is None:
|
|
275
|
+
self.auto_placements = []
|
|
276
|
+
self.auto_placements.append(auto_placement)
|
|
277
|
+
|
|
166
278
|
def add_assignment(self, assignment: TemplateRefAssignment):
|
|
167
279
|
if self.assignments is None:
|
|
168
280
|
self.assignments = []
|
|
@@ -54,9 +54,13 @@ class NormalizedResponse:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class RequestHelper:
|
|
57
|
-
def __init__(
|
|
57
|
+
def __init__(
|
|
58
|
+
self, private_api_key, raise_exceptions=False, security_headers: dict = None
|
|
59
|
+
):
|
|
60
|
+
|
|
58
61
|
self._private_api_key = private_api_key
|
|
59
62
|
self._raise_exceptions = raise_exceptions
|
|
63
|
+
self._security_headers = security_headers
|
|
60
64
|
|
|
61
65
|
def delete(self, url, **kwargs):
|
|
62
66
|
return self._make_request("delete", url, **kwargs)
|
|
@@ -87,6 +91,9 @@ class RequestHelper:
|
|
|
87
91
|
if more_headers:
|
|
88
92
|
hdrs.update(more_headers)
|
|
89
93
|
|
|
94
|
+
if self._security_headers:
|
|
95
|
+
hdrs.update(self._security_headers)
|
|
96
|
+
|
|
90
97
|
hdrs["Authorization"] = f"Token {self._private_api_key}"
|
|
91
98
|
|
|
92
99
|
if content_type is not None:
|
|
@@ -120,4 +127,5 @@ class RequestHelper:
|
|
|
120
127
|
|
|
121
128
|
if self._raise_exceptions:
|
|
122
129
|
response.raise_for_status()
|
|
130
|
+
|
|
123
131
|
return NormalizedResponse(response)
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/bundle.py
RENAMED
|
@@ -103,6 +103,78 @@ class BundleSubClient(SubClient):
|
|
|
103
103
|
files = bdl_helper.files
|
|
104
104
|
return self.create(data=data, files=files)
|
|
105
105
|
|
|
106
|
+
def create_from_envelope_template(self, data: dict) -> NormalizedResponse:
|
|
107
|
+
"""Create a Bundle from an envelope template.
|
|
108
|
+
|
|
109
|
+
This method creates a bundle using a pre-configured envelope template.
|
|
110
|
+
The envelope template contains complete envelope configurations including
|
|
111
|
+
documents, signers, and field assignments.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
data: raw data for Bundle creation from envelope template, expressed as a python dict.
|
|
115
|
+
Must include:
|
|
116
|
+
- packets: list of signer information with keys matching template packet keys
|
|
117
|
+
- envelope_template: dict with template_id and optional field_values
|
|
118
|
+
Optional fields:
|
|
119
|
+
- label: envelope label
|
|
120
|
+
- is_test: whether this is a test envelope (default: False)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
NormalizedResponse object
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
data = {
|
|
127
|
+
"label": "Contract from Envelope Template",
|
|
128
|
+
"is_test": True,
|
|
129
|
+
"packets": [
|
|
130
|
+
{
|
|
131
|
+
"key": "signer-1",
|
|
132
|
+
"name": "John Doe",
|
|
133
|
+
"email": "john@example.com"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"envelope_template": {
|
|
137
|
+
"template_id": "T-xxxxxxxxxxx",
|
|
138
|
+
"field_values": [
|
|
139
|
+
{
|
|
140
|
+
"key": "company_name",
|
|
141
|
+
"initial_value": "ACME Corporation"
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
response = client.bundles.create_from_envelope_template(data)
|
|
147
|
+
"""
|
|
148
|
+
if not data:
|
|
149
|
+
raise ValueError("data is required")
|
|
150
|
+
|
|
151
|
+
url = self.build_url(endpoints.BUNDLES.CREATE_FROM_ENVELOPE_TEMPLATE)
|
|
152
|
+
response = self._requests.post(url, json=data)
|
|
153
|
+
|
|
154
|
+
return response
|
|
155
|
+
|
|
156
|
+
def create_from_envelope_template_helper(
|
|
157
|
+
self, bdl_helper: BundleHelper
|
|
158
|
+
) -> NormalizedResponse:
|
|
159
|
+
"""Create a Bundle from an envelope template using BundleHelper.
|
|
160
|
+
|
|
161
|
+
Provided as a convenience to simplify creating a Bundle from an envelope template.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
bdl_helper: BundleHelper that has been configured with envelope template.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
NormalizedResponse object
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
bh = BundleHelper(label="Contract", is_test=True)
|
|
171
|
+
bh.add_signer(name="John Doe", email="john@example.com", key="signer-1")
|
|
172
|
+
bh.set_envelope_template("T-abc123", {"company_name": "ACME"})
|
|
173
|
+
response = client.bundles.create_from_envelope_template_helper(bh)
|
|
174
|
+
"""
|
|
175
|
+
data = bdl_helper.as_data_for_envelope_template()
|
|
176
|
+
return self.create_from_envelope_template(data=data)
|
|
177
|
+
|
|
106
178
|
def paged_list(
|
|
107
179
|
self,
|
|
108
180
|
page: int = 1,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from blueink import endpoints
|
|
2
|
+
from blueink.paginator import PaginatedIterator
|
|
3
|
+
from blueink.request_helper import NormalizedResponse
|
|
4
|
+
from blueink.subclients.subclient import SubClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvelopeTemplateSubClient(SubClient):
|
|
8
|
+
def paged_list(
|
|
9
|
+
self, page: int = 1, per_page: int = 50, **query_params
|
|
10
|
+
) -> PaginatedIterator:
|
|
11
|
+
"""Return an iterable object containing a list of envelope templates
|
|
12
|
+
|
|
13
|
+
Typical Usage:
|
|
14
|
+
for page in client.envelope_templates.paged_list():
|
|
15
|
+
page.body -> munch of json
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
page: start page (default 1)
|
|
19
|
+
per_page: max # of results per page (default 50)
|
|
20
|
+
query_params: Additional query params to be put onto the request
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
PaginatedIterator object
|
|
24
|
+
"""
|
|
25
|
+
iterator = PaginatedIterator(
|
|
26
|
+
paged_api_function=self.list, page=page, per_page=per_page, **query_params
|
|
27
|
+
)
|
|
28
|
+
return iterator
|
|
29
|
+
|
|
30
|
+
def list(
|
|
31
|
+
self, page: int = None, per_page: int = None, **query_params
|
|
32
|
+
) -> NormalizedResponse:
|
|
33
|
+
"""Return a list of Envelope Templates.
|
|
34
|
+
|
|
35
|
+
Envelope Templates are reusable document workflows that contain predefined
|
|
36
|
+
documents, field layouts, signer roles, and configuration settings.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
page: which page to fetch
|
|
40
|
+
per_page: how many templates to fetch
|
|
41
|
+
query_params: Additional query params to be put onto the request
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
NormalizedResponse object
|
|
45
|
+
"""
|
|
46
|
+
url = self.build_url(endpoints.ENVELOPE_TEMPLATES.LIST)
|
|
47
|
+
return self._requests.get(
|
|
48
|
+
url, params=self.build_params(page, per_page, **query_params)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def retrieve(self, envelope_template_id: str) -> NormalizedResponse:
|
|
52
|
+
"""Return a singular Envelope Template by id.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
envelope_template_id: The ID that uniquely identifies the Envelope Template
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
NormalizedResponse object
|
|
59
|
+
"""
|
|
60
|
+
url = self.build_url(
|
|
61
|
+
endpoints.ENVELOPE_TEMPLATES.RETRIEVE,
|
|
62
|
+
envelope_template_id=envelope_template_id,
|
|
63
|
+
)
|
|
64
|
+
return self._requests.get(url)
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/template.py
RENAMED
|
@@ -5,9 +5,6 @@ from blueink.subclients.subclient import SubClient
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TemplateSubClient(SubClient):
|
|
8
|
-
def __init__(self, base_url, private_api_key):
|
|
9
|
-
super().__init__(base_url, private_api_key)
|
|
10
|
-
|
|
11
8
|
def paged_list(
|
|
12
9
|
self, page: int = 1, per_page: int = 50, **query_params
|
|
13
10
|
) -> PaginatedIterator:
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_bundle_helper.py
RENAMED
|
@@ -186,3 +186,133 @@ class TestBundleHelper(TestCase):
|
|
|
186
186
|
self.assert_equal(compiled_bundle["packets"][0][k], v)
|
|
187
187
|
for k, v in signer02_data.items():
|
|
188
188
|
self.assert_equal(compiled_bundle["packets"][1][k], v)
|
|
189
|
+
|
|
190
|
+
def test_adding_auto_placements(self):
|
|
191
|
+
"""Test adding auto-placement fields to a document"""
|
|
192
|
+
input_data = copy.deepcopy(self.BUNDLE_INIT_DATA)
|
|
193
|
+
url01 = self.DOCUMENT_01_URL
|
|
194
|
+
signer01_data = copy.deepcopy(self.SIGNER_01_DATA)
|
|
195
|
+
|
|
196
|
+
bh = BundleHelper(**input_data)
|
|
197
|
+
doc01_key = bh.add_document_by_url(url01)
|
|
198
|
+
signer01_key = bh.add_signer(**signer01_data)
|
|
199
|
+
|
|
200
|
+
# Add auto-placement for signature
|
|
201
|
+
bh.add_auto_placement(
|
|
202
|
+
document_key=doc01_key,
|
|
203
|
+
kind="sig",
|
|
204
|
+
search="Signature",
|
|
205
|
+
w=20,
|
|
206
|
+
h=5,
|
|
207
|
+
offset_x=-5,
|
|
208
|
+
offset_y=2,
|
|
209
|
+
editors=[signer01_key],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Add auto-placement for input field
|
|
213
|
+
bh.add_auto_placement(
|
|
214
|
+
document_key=doc01_key,
|
|
215
|
+
kind="inp",
|
|
216
|
+
search="Address",
|
|
217
|
+
w=20,
|
|
218
|
+
h=2,
|
|
219
|
+
offset_x=8,
|
|
220
|
+
offset_y=0,
|
|
221
|
+
editors=[signer01_key],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
compiled_bundle = bh.as_data()
|
|
225
|
+
|
|
226
|
+
# Verify document exists
|
|
227
|
+
self.assert_in("documents", compiled_bundle)
|
|
228
|
+
self.assert_len(compiled_bundle["documents"], 1)
|
|
229
|
+
|
|
230
|
+
# Verify auto_placements exist
|
|
231
|
+
self.assert_in("auto_placements", compiled_bundle["documents"][0])
|
|
232
|
+
self.assert_len(compiled_bundle["documents"][0]["auto_placements"], 2)
|
|
233
|
+
|
|
234
|
+
# Verify first auto-placement (signature)
|
|
235
|
+
auto_placement_1 = compiled_bundle["documents"][0]["auto_placements"][0]
|
|
236
|
+
self.assert_equal(auto_placement_1["kind"], "sig")
|
|
237
|
+
self.assert_equal(auto_placement_1["search"], "Signature")
|
|
238
|
+
self.assert_equal(auto_placement_1["w"], 20)
|
|
239
|
+
self.assert_equal(auto_placement_1["h"], 5)
|
|
240
|
+
self.assert_equal(auto_placement_1["offset_x"], -5)
|
|
241
|
+
self.assert_equal(auto_placement_1["offset_y"], 2)
|
|
242
|
+
self.assert_in(signer01_key, auto_placement_1["editors"])
|
|
243
|
+
|
|
244
|
+
# Verify second auto-placement (input)
|
|
245
|
+
auto_placement_2 = compiled_bundle["documents"][0]["auto_placements"][1]
|
|
246
|
+
self.assert_equal(auto_placement_2["kind"], "inp")
|
|
247
|
+
self.assert_equal(auto_placement_2["search"], "Address")
|
|
248
|
+
self.assert_equal(auto_placement_2["w"], 20)
|
|
249
|
+
self.assert_equal(auto_placement_2["h"], 2)
|
|
250
|
+
self.assert_equal(auto_placement_2["offset_x"], 8)
|
|
251
|
+
self.assert_equal(auto_placement_2["offset_y"], 0)
|
|
252
|
+
self.assert_in(signer01_key, auto_placement_2["editors"])
|
|
253
|
+
|
|
254
|
+
def test_auto_placements_with_regular_fields(self):
|
|
255
|
+
"""Test that auto-placements and regular fields can coexist"""
|
|
256
|
+
input_data = copy.deepcopy(self.BUNDLE_INIT_DATA)
|
|
257
|
+
url01 = self.DOCUMENT_01_URL
|
|
258
|
+
signer01_data = copy.deepcopy(self.SIGNER_01_DATA)
|
|
259
|
+
field01_data = copy.deepcopy(self.FIELD_01_DATA)
|
|
260
|
+
|
|
261
|
+
bh = BundleHelper(**input_data)
|
|
262
|
+
doc01_key = bh.add_document_by_url(url01)
|
|
263
|
+
signer01_key = bh.add_signer(**signer01_data)
|
|
264
|
+
|
|
265
|
+
# Add a regular field
|
|
266
|
+
field01_data["document_key"] = doc01_key
|
|
267
|
+
field01_data["editors"].append(signer01_key)
|
|
268
|
+
bh.add_field(**field01_data)
|
|
269
|
+
|
|
270
|
+
# Add an auto-placement
|
|
271
|
+
bh.add_auto_placement(
|
|
272
|
+
document_key=doc01_key,
|
|
273
|
+
kind="sig",
|
|
274
|
+
search="Sign Here",
|
|
275
|
+
w=25,
|
|
276
|
+
h=6,
|
|
277
|
+
offset_x=0,
|
|
278
|
+
offset_y=3,
|
|
279
|
+
editors=[signer01_key],
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
compiled_bundle = bh.as_data()
|
|
283
|
+
|
|
284
|
+
# Verify both fields and auto_placements exist
|
|
285
|
+
self.assert_in("documents", compiled_bundle)
|
|
286
|
+
doc = compiled_bundle["documents"][0]
|
|
287
|
+
|
|
288
|
+
self.assert_in("fields", doc)
|
|
289
|
+
self.assert_len(doc["fields"], 1)
|
|
290
|
+
|
|
291
|
+
self.assert_in("auto_placements", doc)
|
|
292
|
+
self.assert_len(doc["auto_placements"], 1)
|
|
293
|
+
|
|
294
|
+
def test_auto_placement_with_page_restriction(self):
|
|
295
|
+
"""Test auto-placement with page number restriction"""
|
|
296
|
+
input_data = copy.deepcopy(self.BUNDLE_INIT_DATA)
|
|
297
|
+
url01 = self.DOCUMENT_01_URL
|
|
298
|
+
signer01_data = copy.deepcopy(self.SIGNER_01_DATA)
|
|
299
|
+
|
|
300
|
+
bh = BundleHelper(**input_data)
|
|
301
|
+
doc01_key = bh.add_document_by_url(url01)
|
|
302
|
+
signer01_key = bh.add_signer(**signer01_data)
|
|
303
|
+
|
|
304
|
+
# Add auto-placement restricted to page 2
|
|
305
|
+
bh.add_auto_placement(
|
|
306
|
+
document_key=doc01_key,
|
|
307
|
+
kind="sig",
|
|
308
|
+
search="Page 2 Signature",
|
|
309
|
+
w=20,
|
|
310
|
+
h=5,
|
|
311
|
+
page=2,
|
|
312
|
+
editors=[signer01_key],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
compiled_bundle = bh.as_data()
|
|
316
|
+
auto_placement = compiled_bundle["documents"][0]["auto_placements"][0]
|
|
317
|
+
|
|
318
|
+
self.assert_equal(auto_placement["page"], 2)
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_client.py
RENAMED
|
@@ -503,3 +503,29 @@ class TestClientWebhook(TestCase):
|
|
|
503
503
|
# -----------------
|
|
504
504
|
# Secret testing not implemented, will tamper with whoever runs this test suite
|
|
505
505
|
# -----------------
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# -----------------
|
|
509
|
+
# Envelope Template Subclient Tests
|
|
510
|
+
# -----------------
|
|
511
|
+
class TestClientEnvelopeTemplate(TestCase):
|
|
512
|
+
def test_envelope_template_listing(self):
|
|
513
|
+
client = Client(raise_exceptions=False)
|
|
514
|
+
resp = client.envelope_templates.list()
|
|
515
|
+
self.assert_equal(resp.status, 200)
|
|
516
|
+
self.assert_not_none(resp.data)
|
|
517
|
+
|
|
518
|
+
def test_envelope_template_retrieval(self):
|
|
519
|
+
client = Client(raise_exceptions=False)
|
|
520
|
+
|
|
521
|
+
# First get a list to find a valid template ID
|
|
522
|
+
resp_list = client.envelope_templates.list()
|
|
523
|
+
self.assert_equal(resp_list.status, 200)
|
|
524
|
+
|
|
525
|
+
# If there are templates, test retrieval
|
|
526
|
+
if len(resp_list.data) > 0:
|
|
527
|
+
template_id = resp_list.data[0]["id"]
|
|
528
|
+
resp = client.envelope_templates.retrieve(template_id)
|
|
529
|
+
self.assert_equal(resp.status, 200)
|
|
530
|
+
self.assert_not_none(resp.data)
|
|
531
|
+
self.assert_equal(resp.data["id"], template_id)
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: blueink-client-python
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Python Client for Blueink eSignature API
|
|
5
|
+
Home-page: https://github.com/blueinkhq/blueink-client-python
|
|
6
|
+
Author: Blueink
|
|
7
|
+
Author-email: pypi@blueink.com
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/blueinkhq/blueink-client-python/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: munch
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: pydantic
|
|
18
|
+
Requires-Dist: email-validator
|
|
19
|
+
Provides-Extra: munch
|
|
20
|
+
Requires-Dist: munch>=2.5; extra == "munch"
|
|
21
|
+
Provides-Extra: requests
|
|
22
|
+
Requires-Dist: requests>=2.31; extra == "requests"
|
|
23
|
+
Provides-Extra: pydantic
|
|
24
|
+
Requires-Dist: pydantic>=1.9; extra == "pydantic"
|
|
25
|
+
Provides-Extra: email-validator
|
|
26
|
+
Requires-Dist: email-validator>=1.2; extra == "email-validator"
|
|
27
|
+
|
|
1
28
|
# blueink-client-python
|
|
2
29
|

|
|
3
30
|

|
|
@@ -373,6 +400,56 @@ with open("/path/to/file/example.pdf", 'rb') as file:
|
|
|
373
400
|
doc04_key = bh.add_document_by_file(file)
|
|
374
401
|
```
|
|
375
402
|
|
|
403
|
+
#### Auto-Placement Fields
|
|
404
|
+
|
|
405
|
+
Auto-placement fields allow you to automatically search for text in documents and place
|
|
406
|
+
signature/input fields at those locations with optional offsets. This eliminates the need
|
|
407
|
+
to manually specify exact coordinates for fields.
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from blueink import BundleHelper, Client
|
|
411
|
+
|
|
412
|
+
bh = BundleHelper(
|
|
413
|
+
label="Auto-Placement Example",
|
|
414
|
+
email_subject="Please sign",
|
|
415
|
+
is_test=True,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
signer_key = bh.add_signer(name="John Doe", email="john@example.com")
|
|
419
|
+
doc_key = bh.add_document_by_url("https://www.irs.gov/pub/irs-pdf/fw9.pdf")
|
|
420
|
+
|
|
421
|
+
# Add auto-placement field that searches for "Signature" text
|
|
422
|
+
bh.add_auto_placement(
|
|
423
|
+
document_key=doc_key,
|
|
424
|
+
kind="sig", # Field type: signature
|
|
425
|
+
search="Signature", # Text to search for
|
|
426
|
+
w=20, # Width
|
|
427
|
+
h=5, # Height
|
|
428
|
+
offset_x=-5, # Move 5 units left from found text
|
|
429
|
+
offset_y=2, # Move 2 units down from found text
|
|
430
|
+
editors=[signer_key],
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Add auto-placement for an input field
|
|
434
|
+
bh.add_auto_placement(
|
|
435
|
+
document_key=doc_key,
|
|
436
|
+
kind="inp", # Field type: input
|
|
437
|
+
search="Address", # Text to search for
|
|
438
|
+
w=20,
|
|
439
|
+
h=2,
|
|
440
|
+
offset_x=8, # Move 8 units right from found text
|
|
441
|
+
editors=[signer_key],
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
client = Client()
|
|
445
|
+
response = client.bundles.create_from_bundle_helper(bh)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Key benefits of auto-placement:**
|
|
449
|
+
- No need to manually find exact coordinates
|
|
450
|
+
- Works with template documents that have consistent text labels
|
|
451
|
+
- Automatically adjusts to text position in the document
|
|
452
|
+
- Can be combined with regular manually-positioned fields
|
|
376
453
|
|
|
377
454
|
#### Retrieval
|
|
378
455
|
|
|
@@ -16,6 +16,7 @@ src/blueink/model/persons.py
|
|
|
16
16
|
src/blueink/model/webhook.py
|
|
17
17
|
src/blueink/subclients/__init__.py
|
|
18
18
|
src/blueink/subclients/bundle.py
|
|
19
|
+
src/blueink/subclients/envelope_template.py
|
|
19
20
|
src/blueink/subclients/packet.py
|
|
20
21
|
src/blueink/subclients/person.py
|
|
21
22
|
src/blueink/subclients/subclient.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/__init__.py
RENAMED
|
File without changes
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/packet.py
RENAMED
|
File without changes
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/subclient.py
RENAMED
|
File without changes
|
|
File without changes
|
{blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_person_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|