blueink-client-python 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
blueink/bundle_helper.py CHANGED
@@ -558,7 +558,7 @@ class BundleHelper:
558
558
  Bundle as dictionary
559
559
  """
560
560
  bundle = self._compile_bundle(**additional_data)
561
- return bundle.dict(exclude_unset=True, exclude_none=True)
561
+ return bundle.model_dump(exclude_unset=True, exclude_none=True)
562
562
 
563
563
  def as_data_for_envelope_template(self, **additional_data):
564
564
  """Return data for creating a bundle from an envelope template.
@@ -588,8 +588,10 @@ class BundleHelper:
588
588
 
589
589
  packets = list(self._packets.values())
590
590
  result = {
591
- "packets": [p.dict(exclude_unset=True, exclude_none=True) for p in packets],
592
- "envelope_template": self._envelope_template.dict(
591
+ "packets": [
592
+ p.model_dump(exclude_unset=True, exclude_none=True) for p in packets
593
+ ],
594
+ "envelope_template": self._envelope_template.model_dump(
593
595
  exclude_unset=True, exclude_none=True
594
596
  ),
595
597
  }
@@ -627,4 +629,4 @@ class BundleHelper:
627
629
  Bundle as json
628
630
  """
629
631
  bundle = self._compile_bundle(**additional_data)
630
- return bundle.json(exclude_unset=True, exclude_none=True)
632
+ return bundle.model_dump_json(exclude_unset=True, exclude_none=True)
blueink/client.py CHANGED
@@ -11,6 +11,7 @@ from blueink.subclients.envelope_template import EnvelopeTemplateSubClient
11
11
  from blueink.subclients.packet import PacketSubClient
12
12
  from blueink.subclients.person import PersonSubClient
13
13
  from blueink.subclients.template import TemplateSubClient
14
+ from blueink.subclients.verify import VerifySubClient
14
15
  from blueink.subclients.webhook import WebhookSubClient
15
16
 
16
17
 
@@ -74,3 +75,4 @@ class Client:
74
75
  self._base_url, self._request_helper
75
76
  )
76
77
  self.webhooks = WebhookSubClient(self._base_url, self._request_helper)
78
+ self.verify = VerifySubClient(self._base_url, self._request_helper)
blueink/constants.py CHANGED
@@ -94,6 +94,8 @@ EVENT_TYPE = Munch(
94
94
  EVENT_BUNDLE_DOCS_READY="bundle_docs_ready",
95
95
  EVENT_BUNDLE_ERROR="bundle_error",
96
96
  EVENT_BUNDLE_CANCELLED="bundle_cancelled",
97
+ EVENT_BUNDLE_SIGNER_REASSIGNED="bundle_signer_reassigned",
97
98
  EVENT_PACKET_VIEWED="packet_viewed",
98
99
  EVENT_PACKET_COMPLETE="packet_complete",
100
+ EVENT_PACKET_DECLINED="packet_declined",
99
101
  )
blueink/endpoints.py CHANGED
@@ -12,6 +12,9 @@ class BUNDLES:
12
12
  CREATE_PREPARATION_SESSION = "/bundles/preparation_session/"
13
13
  LIST = "/bundles/"
14
14
  RETRIEVE = "/bundles/${bundle_id}/"
15
+ UPDATE = "/bundles/${bundle_id}/"
16
+ SEND = "/bundles/${bundle_id}/send/"
17
+ VALIDATE = "/bundles/${bundle_id}/validate/"
15
18
  CANCEL = "/bundles/${bundle_id}/cancel/"
16
19
  LIST_EVENTS = "/bundles/${bundle_id}/events/"
17
20
  LIST_FILES = "/bundles/${bundle_id}/files/"
@@ -45,6 +48,10 @@ class ENVELOPE_TEMPLATES:
45
48
  RETRIEVE = "/envelope-templates/${envelope_template_id}/"
46
49
 
47
50
 
51
+ class VERIFY:
52
+ CREATE = "/verify/"
53
+
54
+
48
55
  class WEBHOOKS:
49
56
  CREATE = "/webhooks/"
50
57
  LIST = "/webhooks/"
blueink/model/bundles.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import random
2
2
  import string
3
- from typing import List, Optional
3
+ from typing import List, Optional, Union
4
4
 
5
- from pydantic import BaseModel, EmailStr, validator
5
+ from pydantic import BaseModel, ConfigDict, EmailStr, field_validator, model_validator
6
6
 
7
7
  from blueink.constants import DELIVER_VIA, FIELD_KIND
8
8
 
@@ -20,18 +20,17 @@ def generate_key(type, length=5):
20
20
  class AutoPlacement(BaseModel):
21
21
  """Model for auto-placement fields that automatically find and place fields on documents"""
22
22
 
23
- kind: str = ...
24
- search: str = ...
25
- w: int = ...
26
- h: int = ...
23
+ model_config = ConfigDict(extra="allow")
24
+
25
+ kind: str
26
+ search: str
27
+ w: int
28
+ h: int
27
29
  offset_x: Optional[int] = 0
28
30
  offset_y: Optional[int] = 0
29
- editors: Optional[List[str]]
30
- page: Optional[int]
31
- v_attachment_types: Optional[List[str]]
32
-
33
- class Config:
34
- extra = "allow"
31
+ editors: Optional[List[str]] = None
32
+ page: Optional[int] = None
33
+ v_attachment_types: Optional[List[str]] = None
35
34
 
36
35
  @classmethod
37
36
  def create(
@@ -69,11 +68,13 @@ class AutoPlacement(BaseModel):
69
68
  )
70
69
  return obj
71
70
 
72
- @validator("kind")
71
+ @field_validator("kind")
72
+ @classmethod
73
73
  def kind_is_allowed(cls, v):
74
- assert (
75
- v in FIELD_KIND.values()
76
- ), f"AutoPlacement Kind '{v}' not allowed. Must be one of {FIELD_KIND.values()}"
74
+ if v not in FIELD_KIND.values():
75
+ raise ValueError(
76
+ f"AutoPlacement Kind '{v}' not allowed. Must be one of {FIELD_KIND.values()}"
77
+ )
77
78
  return v
78
79
 
79
80
  def add_editor(self, editor: str):
@@ -83,25 +84,24 @@ class AutoPlacement(BaseModel):
83
84
 
84
85
 
85
86
  class Field(BaseModel):
86
- kind: str = ...
87
- key: str = ...
88
- x: int = ...
89
- y: int = ...
90
- w: int = ...
91
- h: int = ...
92
- label: Optional[str]
93
- page: Optional[int]
94
- v_pattern: Optional[int]
95
- v_min: Optional[int]
96
- v_max: Optional[int]
97
- v_regex: Optional[str]
98
- v_regex_msg: Optional[str]
99
- editors: Optional[List[str]]
100
- v_attachment_types: Optional[List[str]]
101
- data_flow_tag: Optional[str]
102
-
103
- class Config:
104
- extra = "allow"
87
+ model_config = ConfigDict(extra="allow")
88
+
89
+ kind: str
90
+ key: str
91
+ x: int
92
+ y: int
93
+ w: int
94
+ h: int
95
+ label: Optional[str] = None
96
+ page: Optional[int] = None
97
+ v_pattern: Optional[int] = None
98
+ v_min: Optional[int] = None
99
+ v_max: Optional[int] = None
100
+ v_regex: Optional[str] = None
101
+ v_regex_msg: Optional[str] = None
102
+ editors: Optional[List[str]] = None
103
+ v_attachment_types: Optional[List[str]] = None
104
+ data_flow_tag: Optional[str] = None
105
105
 
106
106
  @classmethod
107
107
  def create(cls, x, y, w, h, page, kind, key=None, **kwargs):
@@ -110,11 +110,13 @@ class Field(BaseModel):
110
110
  obj = Field(key=key, x=x, y=y, w=w, h=h, page=page, kind=kind, **kwargs)
111
111
  return obj
112
112
 
113
- @validator("kind")
113
+ @field_validator("kind")
114
+ @classmethod
114
115
  def kind_is_allowed(cls, v):
115
- assert (
116
- v in FIELD_KIND.values()
117
- ), f"Field Kind '{v}' not allowed. Must be one of {FIELD_KIND.values()}"
116
+ if v not in FIELD_KIND.values():
117
+ raise ValueError(
118
+ f"Field Kind '{v}' not allowed. Must be one of {FIELD_KIND.values()}"
119
+ )
118
120
  return v
119
121
 
120
122
  def add_editor(self, editor: str):
@@ -124,26 +126,26 @@ class Field(BaseModel):
124
126
 
125
127
 
126
128
  class Packet(BaseModel):
127
- key: str = ...
128
- name: str = ...
129
- email: Optional[EmailStr]
130
- phone: Optional[str]
131
- auth_sms: Optional[bool]
132
- auth_selfie: Optional[bool]
133
- auth_id: Optional[bool]
134
- deliver_via: Optional[str]
135
- person_id: Optional[str]
136
- order: Optional[str]
137
- requires_witness: Optional[bool]
138
- witness_nominated_by: Optional[str]
139
-
140
- class Config:
141
- extra = "allow"
142
-
143
- @validator("deliver_via")
129
+ model_config = ConfigDict(extra="allow")
130
+
131
+ key: str
132
+ name: str
133
+ email: Optional[EmailStr] = None
134
+ phone: Optional[str] = None
135
+ auth_sms: Optional[bool] = None
136
+ auth_selfie: Optional[bool] = None
137
+ auth_id: Optional[bool] = None
138
+ deliver_via: Optional[str] = None
139
+ person_id: Optional[str] = None
140
+ order: Optional[str] = None
141
+ requires_witness: Optional[bool] = None
142
+ witness_nominated_by: Optional[str] = None
143
+
144
+ @field_validator("deliver_via")
145
+ @classmethod
144
146
  def deliver_via_is_allowed(cls, v):
145
- if v is not None:
146
- assert v in DELIVER_VIA.values(), (
147
+ if v is not None and v not in DELIVER_VIA.values():
148
+ raise ValueError(
147
149
  f"deliver_via '{v}' not allowed. Must be None"
148
150
  f" or one of {DELIVER_VIA.values()}"
149
151
  )
@@ -158,11 +160,10 @@ class Packet(BaseModel):
158
160
 
159
161
 
160
162
  class TemplateRefAssignment(BaseModel):
161
- role: str = ...
162
- signer: str = ...
163
+ model_config = ConfigDict(extra="allow")
163
164
 
164
- class Config:
165
- extra = "allow"
165
+ role: str
166
+ signer: str
166
167
 
167
168
  @classmethod
168
169
  def create(cls, role, signer, **kwargs):
@@ -171,11 +172,10 @@ class TemplateRefAssignment(BaseModel):
171
172
 
172
173
 
173
174
  class TemplateRefFieldValue(BaseModel):
174
- key: str = ...
175
- initial_value: str = ...
175
+ model_config = ConfigDict(extra="allow")
176
176
 
177
- class Config:
178
- extra = "allow"
177
+ key: str
178
+ initial_value: str
179
179
 
180
180
  @classmethod
181
181
  def create(cls, key, initial_value, **kwargs):
@@ -186,11 +186,10 @@ class TemplateRefFieldValue(BaseModel):
186
186
  class EnvelopeTemplateFieldValue(BaseModel):
187
187
  """Model for field values in envelope templates"""
188
188
 
189
- key: str = ...
190
- initial_value: str = ...
189
+ model_config = ConfigDict(extra="allow")
191
190
 
192
- class Config:
193
- extra = "allow"
191
+ key: str
192
+ initial_value: str
194
193
 
195
194
  @classmethod
196
195
  def create(cls, key, initial_value, **kwargs):
@@ -201,11 +200,10 @@ class EnvelopeTemplateFieldValue(BaseModel):
201
200
  class EnvelopeTemplate(BaseModel):
202
201
  """Model for envelope template reference"""
203
202
 
204
- template_id: str = ...
205
- field_values: Optional[List[EnvelopeTemplateFieldValue]]
203
+ model_config = ConfigDict(extra="allow")
206
204
 
207
- class Config:
208
- extra = "allow"
205
+ template_id: str
206
+ field_values: Optional[List[EnvelopeTemplateFieldValue]] = None
209
207
 
210
208
  @classmethod
211
209
  def create(cls, template_id, field_values=None, **kwargs):
@@ -221,12 +219,11 @@ class EnvelopeTemplate(BaseModel):
221
219
 
222
220
 
223
221
  class TemplateRef(BaseModel):
224
- template_id: Optional[str]
225
- assignments: Optional[List[TemplateRefAssignment]]
226
- field_values: Optional[List[TemplateRefFieldValue]]
222
+ model_config = ConfigDict(extra="allow")
227
223
 
228
- class Config:
229
- extra = "allow"
224
+ template_id: Optional[str] = None
225
+ assignments: Optional[List[TemplateRefAssignment]] = None
226
+ field_values: Optional[List[TemplateRefFieldValue]] = None
230
227
 
231
228
  @classmethod
232
229
  def create(cls, key=None, **kwargs):
@@ -247,20 +244,19 @@ class TemplateRef(BaseModel):
247
244
 
248
245
 
249
246
  class Document(BaseModel):
250
- key: str = ...
247
+ model_config = ConfigDict(extra="allow")
248
+
249
+ key: str
251
250
 
252
251
  # document related
253
- file_url: Optional[str]
254
- filename: Optional[str]
255
- file_b64: Optional[str]
256
- file_html: Optional[str]
257
- file_index: Optional[int]
258
- fields: Optional[List[Field]]
259
- auto_placements: Optional[List[AutoPlacement]]
260
- html_fields_mode: Optional[str]
261
-
262
- class Config:
263
- extra = "allow"
252
+ file_url: Optional[str] = None
253
+ filename: Optional[str] = None
254
+ file_b64: Optional[str] = None
255
+ file_html: Optional[str] = None
256
+ file_index: Optional[int] = None
257
+ fields: Optional[List[Field]] = None
258
+ auto_placements: Optional[List[AutoPlacement]] = None
259
+ html_fields_mode: Optional[str] = None
264
260
 
265
261
  @classmethod
266
262
  def create(cls, key=None, **kwargs):
@@ -297,22 +293,20 @@ class ImportedDocument(BaseModel):
297
293
  Providing both or neither will raise a ValidationError.
298
294
  """
299
295
 
300
- key: Optional[str]
301
- filename: Optional[str]
302
- file_b64: Optional[str]
303
- file_html: Optional[str]
304
- file_url: Optional[str]
305
- file_index: Optional[int]
306
- fields: Optional[list]
296
+ model_config = ConfigDict(extra="allow")
307
297
 
308
- class Config:
309
- extra = "allow"
298
+ key: Optional[str] = None
299
+ filename: Optional[str] = None
300
+ file_b64: Optional[str] = None
301
+ file_html: Optional[str] = None
302
+ file_url: Optional[str] = None
303
+ file_index: Optional[int] = None
304
+ fields: Optional[list] = None
310
305
 
311
- @validator("file_html", always=True)
312
- def validate_file_source(cls, file_html, values):
313
- file_b64 = values.get("file_b64")
314
- has_b64 = bool(file_b64)
315
- has_html = bool(file_html)
306
+ @model_validator(mode="after")
307
+ def validate_file_source(self):
308
+ has_b64 = bool(self.file_b64)
309
+ has_html = bool(self.file_html)
316
310
 
317
311
  if has_b64 and has_html:
318
312
  raise ValueError("Provide only one of 'file_b64' or 'file_html', not both")
@@ -320,7 +314,7 @@ class ImportedDocument(BaseModel):
320
314
  if not has_b64 and not has_html:
321
315
  raise ValueError("Each document must have either 'file_b64' or 'file_html'")
322
316
 
323
- return file_html
317
+ return self
324
318
 
325
319
  @classmethod
326
320
  def create(cls, key=None, **kwargs):
@@ -331,25 +325,29 @@ class ImportedDocument(BaseModel):
331
325
 
332
326
 
333
327
  class Bundle(BaseModel):
334
- packets: List[Packet] = ...
335
- documents: List[Document] = ...
336
- label: Optional[str]
337
- in_order: Optional[bool]
338
- email_subject: Optional[str]
339
- email_message: Optional[str]
340
- cc_emails: Optional[List[EmailStr]]
341
- is_test: Optional[bool]
342
- custom_key: Optional[str]
343
- team: Optional[str]
344
- signing_brand: Optional[str]
345
- expires: Optional[str]
346
- tag_values: Optional[dict]
347
-
348
- class Config:
349
- extra = "allow"
328
+ model_config = ConfigDict(extra="allow")
329
+
330
+ packets: List[Packet]
331
+ documents: List[Union[Document, TemplateRef]]
332
+ label: Optional[str] = None
333
+ in_order: Optional[bool] = None
334
+ email_subject: Optional[str] = None
335
+ email_message: Optional[str] = None
336
+ cc_emails: Optional[List[EmailStr]] = None
337
+ is_test: Optional[bool] = None
338
+ custom_key: Optional[str] = None
339
+ team: Optional[str] = None
340
+ signing_brand: Optional[str] = None
341
+ expires: Optional[str] = None
342
+ tag_values: Optional[dict] = None
350
343
 
351
344
  @classmethod
352
- def create(cls, packets: List[Packet], documents: List[Document], **kwargs):
345
+ def create(
346
+ cls,
347
+ packets: List[Packet],
348
+ documents: List[Union[Document, TemplateRef]],
349
+ **kwargs,
350
+ ):
353
351
  obj = Bundle(packets=packets, documents=documents, **kwargs)
354
352
  return obj
355
353
 
blueink/model/persons.py CHANGED
@@ -1,20 +1,18 @@
1
- from pydantic import BaseModel, EmailStr
1
+ from pydantic import BaseModel, ConfigDict, EmailStr
2
2
  from typing import Optional, List
3
3
 
4
4
 
5
5
  class ContactChannelSchema(BaseModel):
6
- email: Optional[EmailStr]
7
- phone: Optional[str]
8
- kind: Optional[str]
6
+ model_config = ConfigDict(extra="allow")
9
7
 
10
- class Config:
11
- extra = "allow"
8
+ email: Optional[EmailStr] = None
9
+ phone: Optional[str] = None
10
+ kind: Optional[str] = None
12
11
 
13
12
 
14
13
  class PersonSchema(BaseModel):
15
- name: Optional[str]
16
- metadata: Optional[dict]
17
- channels: Optional[List[ContactChannelSchema]]
14
+ model_config = ConfigDict(extra="allow")
18
15
 
19
- class Config:
20
- extra = "allow"
16
+ name: Optional[str] = None
17
+ metadata: Optional[dict] = None
18
+ channels: Optional[List[ContactChannelSchema]] = None
blueink/model/webhook.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from typing import List, Optional
2
2
 
3
- from pydantic import BaseModel, Field, validator
3
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
4
4
 
5
5
  from blueink.constants import EVENT_TYPE
6
6
 
7
7
 
8
8
  class WebhookExtraHeaderSchema(BaseModel):
9
- id: Optional[str]
9
+ id: Optional[str] = None
10
10
  webhook: str
11
11
  name: str
12
12
  value: str
@@ -14,17 +14,23 @@ class WebhookExtraHeaderSchema(BaseModel):
14
14
 
15
15
 
16
16
  class WebhookSchema(BaseModel):
17
- id: Optional[str]
17
+ model_config = ConfigDict(populate_by_name=True)
18
+
19
+ id: Optional[str] = None
20
+ name: Optional[str] = None
18
21
  url: str
19
22
  enabled: bool = True
20
23
  json_: bool = Field(default=True, alias="json")
21
24
  event_types: List[str]
22
- extra_headers: Optional[List[WebhookExtraHeaderSchema]]
25
+ extra_headers: Optional[List[WebhookExtraHeaderSchema]] = None
23
26
 
24
- @validator("event_types")
27
+ @field_validator("event_types")
28
+ @classmethod
25
29
  def validate_event_types(cls, event_types):
26
30
  for event_type in event_types:
27
- assert event_type in EVENT_TYPE.values(), (
28
- f"subscription event_type '{event_type}' not allowed. Must be one of "
29
- f"{EVENT_TYPE.values()}"
30
- )
31
+ if event_type not in EVENT_TYPE.values():
32
+ raise ValueError(
33
+ f"subscription event_type '{event_type}' not allowed. Must be one of "
34
+ f"{EVENT_TYPE.values()}"
35
+ )
36
+ return event_types
blueink/person_helper.py CHANGED
@@ -139,7 +139,7 @@ class PersonHelper:
139
139
  metadata=self._metadata,
140
140
  channels=channels,
141
141
  )
142
- out_dict = person_out.dict(
142
+ out_dict = person_out.model_dump(
143
143
  exclude_unset=True,
144
144
  )
145
145
 
@@ -283,7 +283,50 @@ class BundleSubClient(SubClient):
283
283
 
284
284
  """
285
285
  url = self.build_url(endpoints.BUNDLES.CANCEL, bundle_id=bundle_id)
286
- return self._request.put(url)
286
+ return self._requests.put(url)
287
+
288
+ def update(self, bundle_id: str, data: dict) -> NormalizedResponse:
289
+ """Partially update a Bundle (PATCH).
290
+
291
+ Typically used to update fields such as ``signing_brand``, ``team``,
292
+ ``expires``, ``cc_emails`` and reminder settings on an existing bundle.
293
+
294
+ Args:
295
+ bundle_id: bundle slug
296
+ data: dict of fields to update on the bundle
297
+
298
+ Returns:
299
+ NormalizedResponse object
300
+ """
301
+ if not data:
302
+ raise ValueError("data is required")
303
+
304
+ url = self.build_url(endpoints.BUNDLES.UPDATE, bundle_id=bundle_id)
305
+ return self._requests.patch(url, json=data)
306
+
307
+ def send(self, bundle_id: str) -> NormalizedResponse:
308
+ """Send a draft Bundle (POST).
309
+
310
+ Args:
311
+ bundle_id: bundle slug
312
+
313
+ Returns:
314
+ NormalizedResponse object
315
+ """
316
+ url = self.build_url(endpoints.BUNDLES.SEND, bundle_id=bundle_id)
317
+ return self._requests.post(url)
318
+
319
+ def validate(self, bundle_id: str) -> NormalizedResponse:
320
+ """Validate a draft Bundle (PUT).
321
+
322
+ Args:
323
+ bundle_id: bundle slug
324
+
325
+ Returns:
326
+ NormalizedResponse object, with keys ``can_send``, ``msg``, ``n_docs``
327
+ """
328
+ url = self.build_url(endpoints.BUNDLES.VALIDATE, bundle_id=bundle_id)
329
+ return self._requests.put(url)
287
330
 
288
331
  def list_events(self, bundle_id: str) -> NormalizedResponse:
289
332
  """Return a list of events for the supplied bundle corresponding to the id
@@ -54,4 +54,4 @@ class PacketSubClient(SubClient):
54
54
  A NormalizedResponse
55
55
  """
56
56
  url = self.build_url(endpoints.PACKETS.REMIND, packet_id=packet_id)
57
- self._requests.put(url)
57
+ return self._requests.put(url)
@@ -0,0 +1,20 @@
1
+ from blueink import endpoints
2
+ from blueink.request_helper import NormalizedResponse
3
+ from blueink.subclients.subclient import SubClient
4
+
5
+
6
+ class VerifySubClient(SubClient):
7
+ def create(self, data: dict) -> NormalizedResponse:
8
+ """Verify a signed PDF against the Blueink application (POST /verify/).
9
+
10
+ Args:
11
+ data: dict containing the ``hash`` (sha256) of the document to verify
12
+
13
+ Returns:
14
+ NormalizedResponse object, with verification details as `data`
15
+ """
16
+ if not data:
17
+ raise ValueError("data is required")
18
+
19
+ url = self.build_url(endpoints.VERIFY.CREATE)
20
+ return self._requests.post(url, json=data)
@@ -15,9 +15,7 @@ class WebhookSubClient(SubClient):
15
15
  def list(self, **query_params) -> NormalizedResponse:
16
16
  url = self.build_url(endpoint=endpoints.WEBHOOKS.LIST)
17
17
 
18
- return self._requests.get(
19
- url, params=self.build_params(query_params=query_params)
20
- )
18
+ return self._requests.get(url, params=self.build_params(**query_params))
21
19
 
22
20
  def retrieve(self, webhook_id: str) -> NormalizedResponse:
23
21
  url = self.build_url(
@@ -58,12 +56,12 @@ class WebhookSubClient(SubClient):
58
56
 
59
57
  def delete_header(self, header_id: str) -> NormalizedResponse:
60
58
  url = self.build_url(
61
- endpoint=endpoints.WEBHOOKS.DELETE_HEADERE, webhook_header_id=header_id
59
+ endpoint=endpoints.WEBHOOKS.DELETE_HEADER, webhook_header_id=header_id
62
60
  )
63
61
 
64
62
  return self._requests.delete(url)
65
63
 
66
- def update_header(self, header_id: str, **data) -> NormalizedResponse:
64
+ def update_header(self, header_id: str, data: dict) -> NormalizedResponse:
67
65
  url = self.build_url(
68
66
  endpoint=endpoints.WEBHOOKS.UPDATE_HEADER, webhook_header_id=header_id
69
67
  )
@@ -76,13 +74,11 @@ class WebhookSubClient(SubClient):
76
74
  def list_events(self, **query_params) -> NormalizedResponse:
77
75
  url = self.build_url(endpoint=endpoints.WEBHOOKS.LIST_EVENTS)
78
76
 
79
- return self._requests.get(
80
- url, params=self.build_params(query_params=query_params)
81
- )
77
+ return self._requests.get(url, params=self.build_params(**query_params))
82
78
 
83
79
  def retrieve_event(self, event_id: str) -> NormalizedResponse:
84
80
  url = self.build_url(
85
- endpoint=endpoints.WEBHOOKS.RETRIEVE_EVENT, wehook_event_id=event_id
81
+ endpoint=endpoints.WEBHOOKS.RETRIEVE_EVENT, webhook_event_id=event_id
86
82
  )
87
83
 
88
84
  return self._requests.get(url)
@@ -93,9 +89,7 @@ class WebhookSubClient(SubClient):
93
89
  def list_deliveries(self, **query_params) -> NormalizedResponse:
94
90
  url = self.build_url(endpoint=endpoints.WEBHOOKS.LIST_DELIVERIES)
95
91
 
96
- return self._requests.get(
97
- url, params=self.build_params(query_params=query_params)
98
- )
92
+ return self._requests.get(url, params=self.build_params(**query_params))
99
93
 
100
94
  def retrieve_delivery(self, delivery_id: str) -> NormalizedResponse:
101
95
  url = self.build_url(
@@ -543,16 +543,12 @@ class TestClientEndpoints(TestCase):
543
543
 
544
544
  def test_bundle_preparation_session_url(self):
545
545
  client = self._client()
546
- url = client.bundles.build_url(
547
- endpoints.BUNDLES.CREATE_PREPARATION_SESSION
548
- )
546
+ url = client.bundles.build_url(endpoints.BUNDLES.CREATE_PREPARATION_SESSION)
549
547
  self.assert_equal(url, f"{self.BASE_URL}/bundles/preparation_session/")
550
548
 
551
549
  def test_template_preparation_session_url(self):
552
550
  client = self._client()
553
- url = client.templates.build_url(
554
- endpoints.TEMPLATES.CREATE_PREPARATION_SESSION
555
- )
551
+ url = client.templates.build_url(endpoints.TEMPLATES.CREATE_PREPARATION_SESSION)
556
552
  self.assert_equal(url, f"{self.BASE_URL}/templates/preparation_session/")
557
553
 
558
554
  def test_template_update_url(self):
@@ -561,3 +557,27 @@ class TestClientEndpoints(TestCase):
561
557
  endpoints.TEMPLATES.UPDATE, template_id="T-abc123"
562
558
  )
563
559
  self.assert_equal(url, f"{self.BASE_URL}/templates/T-abc123/")
560
+
561
+ def test_bundle_update_url(self):
562
+ client = self._client()
563
+ url = client.bundles.build_url(endpoints.BUNDLES.UPDATE, bundle_id="B-abc123")
564
+ self.assert_equal(url, f"{self.BASE_URL}/bundles/B-abc123/")
565
+
566
+ def test_bundle_send_url(self):
567
+ client = self._client()
568
+ url = client.bundles.build_url(endpoints.BUNDLES.SEND, bundle_id="B-abc123")
569
+ self.assert_equal(url, f"{self.BASE_URL}/bundles/B-abc123/send/")
570
+
571
+ def test_bundle_validate_url(self):
572
+ client = self._client()
573
+ url = client.bundles.build_url(endpoints.BUNDLES.VALIDATE, bundle_id="B-abc123")
574
+ self.assert_equal(url, f"{self.BASE_URL}/bundles/B-abc123/validate/")
575
+
576
+ def test_verify_url(self):
577
+ client = self._client()
578
+ url = client.verify.build_url(endpoints.VERIFY.CREATE)
579
+ self.assert_equal(url, f"{self.BASE_URL}/verify/")
580
+
581
+ def test_verify_subclient_registered(self):
582
+ client = self._client()
583
+ self.assert_not_none(client.verify)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blueink-client-python
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Python Client for Blueink eSignature API
5
5
  Home-page: https://github.com/blueinkhq/blueink-client-python
6
6
  Author: Blueink
@@ -14,16 +14,16 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: munch
16
16
  Requires-Dist: requests
17
- Requires-Dist: pydantic
18
- Requires-Dist: email-validator
17
+ Requires-Dist: pydantic<3.0,>=2.0
18
+ Requires-Dist: email-validator<3.0,>=2.0
19
19
  Provides-Extra: munch
20
20
  Requires-Dist: munch>=2.5; extra == "munch"
21
21
  Provides-Extra: requests
22
22
  Requires-Dist: requests>=2.31; extra == "requests"
23
23
  Provides-Extra: pydantic
24
- Requires-Dist: pydantic>=1.9; extra == "pydantic"
24
+ Requires-Dist: pydantic<3.0,>=2.0; extra == "pydantic"
25
25
  Provides-Extra: email-validator
26
- Requires-Dist: 1.2; extra == "email-validator"
26
+ Requires-Dist: email-validator<3.0,>=2.0; extra == "email-validator"
27
27
  Dynamic: license-file
28
28
 
29
29
  # blueink-client-python
@@ -124,6 +124,7 @@ The supported "resources" are:
124
124
  * `persons`
125
125
  * `packets`
126
126
  * `templates`
127
+ * `envelope_templates`
127
128
  * `webhooks`
128
129
 
129
130
  The methods correspond to common REST operations:
@@ -266,6 +267,8 @@ as kwargs.
266
267
 
267
268
  ### Bundle Related
268
269
  * Create via ```client.bundles.create(...)``` or ```client.bundles.create_from_bundle_helper(...)```
270
+ * Create from Envelope Template via ```client.bundles.create_from_envelope_template_helper(...)``` or ```client.bundles.create_from_envelope_template(...)```
271
+ * Create Preparation Session via ```client.bundles.create_preparation_session(...)```
269
272
  * List via ```client.bundles.list(...)``` or ```client.bundles.paged_list(...)```
270
273
  * Retrieve via ```client.bundles.retrieve(...)```
271
274
  * Cancel via ```client.bundles.cancel(...)```
@@ -289,6 +292,12 @@ as kwargs.
289
292
  ### Template Related
290
293
  * List via ```client.templates.list(...)``` or ```client.templates.paged_list(...)```
291
294
  * Retrieve via ```client.templates.retrieve(...)```
295
+ * Update via ```client.templates.update(...)```
296
+ * Create Preparation Session via ```client.templates.create_preparation_session(...)```
297
+
298
+ ### Envelope Template Related
299
+ * List via ```client.envelope_templates.list(...)``` or ```client.envelope_templates.paged_list(...)```
300
+ * Retrieve via ```client.envelope_templates.retrieve(...)```
292
301
 
293
302
  ### Webhook Related
294
303
 
@@ -452,6 +461,19 @@ response = client.bundles.create_from_bundle_helper(bh)
452
461
  - Automatically adjusts to text position in the document
453
462
  - Can be combined with regular manually-positioned fields
454
463
 
464
+ #### Bundle Tag Values
465
+
466
+ Bundle-level `tag_values` can be set on the `BundleHelper` constructor to attach
467
+ arbitrary key/value metadata to a Bundle at creation time:
468
+
469
+ ```python
470
+ bh = BundleHelper(
471
+ label="Tagged Agreement",
472
+ is_test=True,
473
+ tag_values={"contract_type": "nda"},
474
+ )
475
+ ```
476
+
455
477
  #### Retrieval
456
478
 
457
479
  Getting a single bundle is fairly easy. They can be accessed with a single call. To get
@@ -688,9 +710,64 @@ for page in client.templates.paged_list():
688
710
  page.data # templates in page
689
711
  # single
690
712
  template_response = client.templates.retrieve(template_id)
713
+ ```
714
+
715
+ Templates can also be partially updated (for example, to write template metadata):
716
+
717
+ ```python
718
+ client.templates.update(template_id, {"metadata": {"key": "value"}})
719
+ ```
720
+
721
+ #### Template Preparation Sessions
722
+
723
+ A preparation session creates a secure, time-limited URL that can be embedded in an
724
+ iframe to let an end user prepare a template through your application:
725
+
726
+ ```python
727
+ response = client.templates.create_preparation_session({
728
+ "template_id": template_id, # optional: slug of an existing template to edit
729
+ "redirect_url": "https://your-app.example.com/done", # optional
730
+ })
731
+ prep_url = response.data.url # URL to embed in an iframe
732
+ expires = response.data.expires # ISO 8601 timestamp when the URL expires
733
+ ```
734
+
735
+ A similar session can be created for bundle/document preparation via
736
+ `client.bundles.create_preparation_session(...)`.
737
+
738
+ ### Envelope Templates
739
+
740
+ Envelope Templates are reusable document workflows that contain predefined documents,
741
+ field layouts, signer roles, and configuration settings. They can be listed (non-paged),
742
+ listed (paged) or retrieved singly:
743
+
744
+ ```python
745
+ # non paged
746
+ envelope_templates_response = client.envelope_templates.list()
747
+ # paged
748
+ for page in client.envelope_templates.paged_list():
749
+ page.data # envelope templates in page
750
+ # single
751
+ envelope_template_response = client.envelope_templates.retrieve(envelope_template_id)
752
+ ```
691
753
 
754
+ To create a Bundle from an envelope template, configure a `BundleHelper` with the
755
+ template and its signers, then create the Bundle:
692
756
 
757
+ ```python
758
+ from blueink import BundleHelper, Client
759
+
760
+ bh = BundleHelper(label="Contract", is_test=True)
761
+ bh.add_signer(name="John Doe", email="john@example.com", key="signer-1")
762
+ bh.set_envelope_template(
763
+ template_id="T-abc123",
764
+ field_values={"company_name": "ACME Corp"},
765
+ )
766
+
767
+ client = Client()
768
+ response = client.bundles.create_from_envelope_template_helper(bh)
693
769
  ```
770
+
694
771
  ### Webhooks
695
772
 
696
773
  Webhooks can be interacted with via several methods. Webhooks also have related objects, such as
@@ -701,8 +778,8 @@ methods to interact with.
701
778
  from copy import deepcopy
702
779
 
703
780
  from requests import HTTPError
704
- from src.blueink import Client
705
- from src.blueink.constants import EVENT_TYPE
781
+ from blueink import Client
782
+ from blueink.constants import EVENT_TYPE
706
783
 
707
784
  WEBHOOK_01 = {
708
785
  "url": "https://www.example.com/01/",
@@ -0,0 +1,32 @@
1
+ blueink/__init__.py,sha256=2UQx6cM2oh8-osuqB9N8zmfTWlkJ0mTn-6i3F5S44mI,267
2
+ blueink/bundle_helper.py,sha256=Z9eKd4lVJL50JDe8JLWiaXfsE54eVv6D7BPZgdTzlkg,20385
3
+ blueink/client.py,sha256=vRsDjZhtxEljqNAt7j-9072j9do8j4YGa_nzpMx3f6Q,3077
4
+ blueink/constants.py,sha256=RQIXXqs2KHXmrUUY_9kq9bT9RT_J0eziW4AGNgB9lMU,1946
5
+ blueink/endpoints.py,sha256=Yr5U_LeH-lbwINtZk7oFYM9ylHpgx3EvhXOB2k1V9BY,2611
6
+ blueink/paginator.py,sha256=WIdYFdAsdXVYVBmQwmyXkChAHkM5Kr2jGDBEIVaihfY,1754
7
+ blueink/person_helper.py,sha256=iZKzh_zl9G80PKo-YERKyKjnYY7WmPDVvHz351k-VJk,3897
8
+ blueink/request_helper.py,sha256=704lm-ZIqW3eebg-dl5sYubeXaEt_F3LZyVwuAY4dr8,3893
9
+ blueink/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ blueink/model/bundles.py,sha256=KB-i7goTrOobsgV5U1S2rwqlJGnD4_fKXVeCpzKE17E,10290
11
+ blueink/model/persons.py,sha256=SnCbCGo97bkk-sR0il70-O2ssq5UgY_w1YX9ko3_6mc,478
12
+ blueink/model/webhook.py,sha256=G8VmmR5fwXmLDFVm-xxwk1wAbTOlO5iGHHtpDJoRRHw,1021
13
+ blueink/subclients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ blueink/subclients/bundle.py,sha256=tSBfyDScSuhQHAJ-Nnu9Nw-bEUKoI274UcxZT0ycxKg,13612
15
+ blueink/subclients/envelope_template.py,sha256=dLXSOyXSoSjAEaJRvYs1Yf_iujgjFV-jNFZ8TTO239Y,2205
16
+ blueink/subclients/packet.py,sha256=NTxikTbaDf1okdLWR0c9Vcr3UJGkJzn5lu_0loGHmvk,1995
17
+ blueink/subclients/person.py,sha256=MzJt4fInfkxwdM5fnb_T0xAuqweiU8k83unsrAfS8Pw,3822
18
+ blueink/subclients/subclient.py,sha256=yk5Zk3H-CiVgJkDeTMqjp65Z4jz6Jppti9rALo7ttxg,1692
19
+ blueink/subclients/template.py,sha256=UpQXwlLzLOH-emT-bXsxeYDD02x4OK7WBbs-xTJZn8w,3375
20
+ blueink/subclients/verify.py,sha256=bmi-y7G9fP2fEp_uR4OTa2APrnHzLuK6qhGSLY3uO78,685
21
+ blueink/subclients/webhook.py,sha256=lvF0rrczPHTuLIapxq2a1kCzXwtP7IcoYrskdHBou9M,3804
22
+ blueink/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ blueink/tests/test_bundle_helper.py,sha256=S69bMI9RYr1ubeP4GP9pJ9gic7CWocpfrnl2I_aeqZM,23016
24
+ blueink/tests/test_client.py,sha256=7dgeI1Vun9G0xu3Kno4gnHnd14Hsh--hZksj9GbyLSE,20427
25
+ blueink/tests/test_person_helper.py,sha256=ie4UmIiEt3hT6oRp91FN1Uj6P6D7_U3M1zPxBr5NPU4,1900
26
+ blueink/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ blueink/utils/testcase.py,sha256=K_4ff5U0oh8mkOqvdXY0NWEaaJ5qnzJnxIN6vmISie4,1027
28
+ blueink_client_python-1.3.0.dist-info/licenses/LICENSE,sha256=2O7Tj4oNAFf3vQhgcbYenxAcanqcAjOmA9kHp1Y_zDc,1094
29
+ blueink_client_python-1.3.0.dist-info/METADATA,sha256=0cNS3Md8Ii7MDiTnXRkNulB4g_EcOt4RIp90I2ZBggw,26966
30
+ blueink_client_python-1.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
31
+ blueink_client_python-1.3.0.dist-info/top_level.txt,sha256=9hbus8lB_HAEBjA8Vz2_ZKcm8mdU9Bo6V8raVwZPfFs,8
32
+ blueink_client_python-1.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,31 +0,0 @@
1
- blueink/__init__.py,sha256=2UQx6cM2oh8-osuqB9N8zmfTWlkJ0mTn-6i3F5S44mI,267
2
- blueink/bundle_helper.py,sha256=kMnJ6xHA3kPTnAdpx-ZlmxATwRsOnE7AG9u3f2nT5iM,20326
3
- blueink/client.py,sha256=Vmb3RL3A9QwNcMYaQGWkC7k91q18vACWY-rJHPeA4Pk,2947
4
- blueink/constants.py,sha256=NhlzcWvx4vlg4p163yF6aOab4Ev_HvADlBWCbLQzxKQ,1838
5
- blueink/endpoints.py,sha256=PxnOB7lpQy9WWR3fgwkA0hbhM-2F0t14vXJyJX6W6uY,2443
6
- blueink/paginator.py,sha256=WIdYFdAsdXVYVBmQwmyXkChAHkM5Kr2jGDBEIVaihfY,1754
7
- blueink/person_helper.py,sha256=FJtzv4d0RJLtsI8kOXIQJlX6SyKmjpPxmvcuCRAcsXI,3891
8
- blueink/request_helper.py,sha256=704lm-ZIqW3eebg-dl5sYubeXaEt_F3LZyVwuAY4dr8,3893
9
- blueink/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- blueink/model/bundles.py,sha256=kptX6_AnmBvXRQOepaplkutrszC_wR_fF-jUCiRM1Ec,9801
11
- blueink/model/persons.py,sha256=Lg84UK8hjokcLcmr0JA7c1LNGHxGSDGZ_xwpN45HUMI,418
12
- blueink/model/webhook.py,sha256=lzNjwdpvzY5My87HLuNvvZ_lVgnoA5RG3fWSK-jlT5M,803
13
- blueink/subclients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- blueink/subclients/bundle.py,sha256=UNcLD4f7rQryz_nZujBFI-s7uInWRO0wJlxTjJpRbuw,12245
15
- blueink/subclients/envelope_template.py,sha256=dLXSOyXSoSjAEaJRvYs1Yf_iujgjFV-jNFZ8TTO239Y,2205
16
- blueink/subclients/packet.py,sha256=S1y1ab4tg5a9I-PKlp5qhuxbGWPUtl33RyZ2iLPazI0,1988
17
- blueink/subclients/person.py,sha256=MzJt4fInfkxwdM5fnb_T0xAuqweiU8k83unsrAfS8Pw,3822
18
- blueink/subclients/subclient.py,sha256=yk5Zk3H-CiVgJkDeTMqjp65Z4jz6Jppti9rALo7ttxg,1692
19
- blueink/subclients/template.py,sha256=UpQXwlLzLOH-emT-bXsxeYDD02x4OK7WBbs-xTJZn8w,3375
20
- blueink/subclients/webhook.py,sha256=igcreILfcxyLCyAN7zJaTbqeMHFY9FPaq1WLp4kWbT0,3899
21
- blueink/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- blueink/tests/test_bundle_helper.py,sha256=S69bMI9RYr1ubeP4GP9pJ9gic7CWocpfrnl2I_aeqZM,23016
23
- blueink/tests/test_client.py,sha256=xHMgmYxPs6JxlUgI73UmY93FmTpYbCT2Bx67uZvHyJQ,19465
24
- blueink/tests/test_person_helper.py,sha256=ie4UmIiEt3hT6oRp91FN1Uj6P6D7_U3M1zPxBr5NPU4,1900
25
- blueink/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- blueink/utils/testcase.py,sha256=K_4ff5U0oh8mkOqvdXY0NWEaaJ5qnzJnxIN6vmISie4,1027
27
- blueink_client_python-1.2.0.dist-info/licenses/LICENSE,sha256=2O7Tj4oNAFf3vQhgcbYenxAcanqcAjOmA9kHp1Y_zDc,1094
28
- blueink_client_python-1.2.0.dist-info/METADATA,sha256=mDE3nheOoIsgdCHHE_RumwsY5k4xQGYHn9TiuUUFXe0,24150
29
- blueink_client_python-1.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
30
- blueink_client_python-1.2.0.dist-info/top_level.txt,sha256=9hbus8lB_HAEBjA8Vz2_ZKcm8mdU9Bo6V8raVwZPfFs,8
31
- blueink_client_python-1.2.0.dist-info/RECORD,,