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.
Files changed (36) hide show
  1. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/PKG-INFO +53 -3
  2. blueink-client-python-1.0.0/src/blueink_client_python.egg-info/PKG-INFO → blueink_client_python-1.0.1/README.md +50 -27
  3. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/setup.cfg +9 -5
  4. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/bundle_helper.py +183 -0
  5. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/client.py +14 -4
  6. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/endpoints.py +6 -0
  7. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/bundles.py +112 -0
  8. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/request_helper.py +9 -1
  9. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/bundle.py +72 -0
  10. blueink_client_python-1.0.1/src/blueink/subclients/envelope_template.py +64 -0
  11. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/person.py +1 -1
  12. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/template.py +0 -3
  13. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/webhook.py +0 -3
  14. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_bundle_helper.py +130 -0
  15. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_client.py +26 -0
  16. blueink-client-python-1.0.0/README.md → blueink_client_python-1.0.1/src/blueink_client_python.egg-info/PKG-INFO +77 -0
  17. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/SOURCES.txt +1 -0
  18. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/requires.txt +3 -3
  19. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/LICENSE +0 -0
  20. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/pyproject.toml +0 -0
  21. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/__init__.py +0 -0
  22. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/constants.py +0 -0
  23. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/__init__.py +0 -0
  24. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/persons.py +0 -0
  25. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/model/webhook.py +0 -0
  26. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/paginator.py +0 -0
  27. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/person_helper.py +0 -0
  28. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/__init__.py +0 -0
  29. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/packet.py +0 -0
  30. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/subclients/subclient.py +0 -0
  31. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/__init__.py +0 -0
  32. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/tests/test_person_helper.py +0 -0
  33. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/utils/__init__.py +0 -0
  34. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink/utils/testcase.py +0 -0
  35. {blueink-client-python-1.0.0 → blueink_client_python-1.0.1}/src/blueink_client_python.egg-info/dependency_links.txt +0 -0
  36. {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.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.27; extra == "requests"
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
  ![Tests](https://github.com/blueinkhq/blueink-client-python/actions/workflows/helper-tests.yml/badge.svg)
@@ -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
  ![Tests](https://github.com/blueinkhq/blueink-client-python/actions/workflows/helper-tests.yml/badge.svg)
30
3
  ![Style Checks](https://github.com/blueinkhq/blueink-client-python/actions/workflows/style-checks.yml/badge.svg)
@@ -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.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 = munch>=2.5;
30
- requests = requests>=2.27;
31
- pydantic = pydantic>=1.9
32
- email-validator> = 1.2
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(private_api_key, raise_exceptions)
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__(self, private_api_key, raise_exceptions=False):
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)
@@ -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)
@@ -45,7 +45,7 @@ class PersonSubClient(SubClient):
45
45
 
46
46
  Typical Usage:
47
47
  for page in client.persons.paged_list():
48
- page.body -> munch of json
48
+ page.data -> response data
49
49
 
50
50
  Args:
51
51
  page: start page (default 1)
@@ -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:
@@ -4,9 +4,6 @@ from blueink.subclients.subclient import SubClient
4
4
 
5
5
 
6
6
  class WebhookSubClient(SubClient):
7
- def __init__(self, base_url, private_api_key):
8
- super().__init__(base_url, private_api_key)
9
-
10
7
  # ----------
11
8
  # Webhooks
12
9
  # ----------
@@ -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)
@@ -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
  ![Tests](https://github.com/blueinkhq/blueink-client-python/actions/workflows/helper-tests.yml/badge.svg)
3
30
  ![Style Checks](https://github.com/blueinkhq/blueink-client-python/actions/workflows/style-checks.yml/badge.svg)
@@ -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
@@ -3,8 +3,8 @@ requests
3
3
  pydantic
4
4
  email-validator
5
5
 
6
- [email-validator>]
7
- 1.2
6
+ [email-validator]
7
+ email-validator>=1.2
8
8
 
9
9
  [munch]
10
10
  munch>=2.5
@@ -13,4 +13,4 @@ munch>=2.5
13
13
  pydantic>=1.9
14
14
 
15
15
  [requests]
16
- requests>=2.27
16
+ requests>=2.31