formsmarts 2.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Syronex LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,427 @@
1
+ Metadata-Version: 2.4
2
+ Name: formsmarts
3
+ Version: 2.1.0
4
+ Summary: FormSmarts API & Webhook Client
5
+ Author-email: Syronex LLC <api-client@formsmarts.net>
6
+ Project-URL: Homepage, https://formsmarts.com/api-webhook-client
7
+ Project-URL: Bug Tracker, https://formsmarts.com/form-builder-support#contact
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: urllib3>=1.0.0
15
+ Requires-Dist: pyjwt>=2.0.0
16
+ Dynamic: license-file
17
+
18
+ # FormSmarts API & Webhook Client
19
+
20
+ A Python client for the [FormSmarts](https://formsmarts.com) API and webhook system. Automate form workflows: search and edit submissions, download attachments and PDFs, submit forms programmatically, pre-populate form URLs, and verify incoming webhook callbacks.
21
+
22
+ ## Requirements
23
+
24
+ - Python 3.8 or later
25
+ - [urllib3](https://urllib3.readthedocs.io/)
26
+ - [PyJWT](https://pyjwt.readthedocs.io/)
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install formsmarts
32
+ ```
33
+
34
+ ## Authentication
35
+
36
+ All API operations require an `APIAuthenticator` object, initialised with your FormSmarts **Account ID** and **API Key**. You can find both in the [Security Settings](https://formsmarts.com/account/view#security-settings) of your account.
37
+
38
+ ```python
39
+ from formsmarts import APIAuthenticator
40
+
41
+ auth = APIAuthenticator('FSA-999999', 'your-api-key')
42
+ ```
43
+
44
+ > **Security:** Never hard-code credentials. Load them from environment variables or a secrets manager.
45
+
46
+ ```python
47
+ import os
48
+ auth = APIAuthenticator(os.environ['FS_ACCOUNT_ID'], os.environ['FS_API_KEY'])
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Searching & Retrieving Entries
54
+
55
+ ### Search by date range
56
+
57
+ Returns a generator of `FormEntry` objects for all submissions to a form between two dates.
58
+
59
+ ```python
60
+ from formsmarts import APIAuthenticator, FormEntry
61
+
62
+ auth = APIAuthenticator('FSA-999999', 'your-api-key')
63
+
64
+ for entry in FormEntry.search_by_dates(auth, form_id='lqh', start_date='2025-01-01'):
65
+ print(entry.reference_number, entry.submitted_at)
66
+ ```
67
+
68
+ ### Search by email, phone, or ID
69
+
70
+ ```python
71
+ for entry in FormEntry.search(auth, query='jane@example.com'):
72
+ print(entry.reference_number)
73
+ ```
74
+
75
+ Filter results by tag, and exclude entries with specific tags:
76
+
77
+ ```python
78
+ for entry in FormEntry.search(
79
+ auth,
80
+ query='jane@example.com',
81
+ tags='vip',
82
+ exclude_tags=['archived', 'duplicate'],
83
+ ):
84
+ print(entry.reference_number)
85
+ ```
86
+
87
+ Pass `'latest'` as the query to retrieve the most recent submission:
88
+
89
+ ```python
90
+ entry = next(FormEntry.search(auth, query='latest', form_id='lqh'))
91
+ ```
92
+
93
+ ### Fetch a single entry by Reference Number
94
+
95
+ ```python
96
+ entry = FormEntry.fetch(auth, ref_num='ABC-123456')
97
+ ```
98
+
99
+ ### Fetch a batch of entries
100
+
101
+ ```python
102
+ ref_nums = ['ABC-123456', 'ABC-123457', 'ABC-123458']
103
+ for entry in FormEntry.fetch_batch(auth, form_id='lqh', reference_numbers=ref_nums):
104
+ print(entry.reference_number)
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Working with Fields
110
+
111
+ ### Iterate over all fields
112
+
113
+ ```python
114
+ for field in entry.fields:
115
+ print(field.name, field.type, field.value)
116
+ ```
117
+
118
+ ### Look up fields by name, type, or ID
119
+
120
+ ```python
121
+ # By name (returns a list, as names may not be unique)
122
+ email = entry.fields_by_name('Email')[0].value
123
+
124
+ # By datatype
125
+ uploads = entry.fields_by_type('upload')
126
+
127
+ # By field ID
128
+ field = entry.field_by_id(12345)
129
+ ```
130
+
131
+ ### Field types and their values
132
+
133
+ | Type | `Field` subclass | `.value` returns |
134
+ |---|---|---|
135
+ | `text`, `email`, `phone`, … | `Field` | `str` |
136
+ | `boolean` (checkbox) | `Boolean` | `bool` |
137
+ | `number` | `Field` | `Decimal` |
138
+ | `posint` | `Field` | `int` |
139
+ | `date` | `Date` | `datetime.date` |
140
+ | `option` (drop-down, radio) | `Option` | `str` |
141
+ | `upload` | `Upload` | upload ID (`str`) |
142
+ | `signature` | `Signature` | `str` |
143
+ | `country` | `Country` | ISO country code (`str`) |
144
+ | `csd` (country subdivision) | `CSD` | ISO subdivision code (`str`) |
145
+
146
+ #### Country and subdivision names
147
+
148
+ ```python
149
+ country_field = entry.fields_by_type('country')[0]
150
+ print(country_field.country_code) # 'US'
151
+ print(country_field.country_name) # 'United States'
152
+
153
+ csd_field = entry.fields_by_type('csd')[0]
154
+ print(csd_field.subdivision_code) # 'NY'
155
+ print(csd_field.subdivision_name) # 'New York'
156
+ ```
157
+
158
+ ### Edit a field value
159
+
160
+ Assigning to `field.value` immediately persists the change via the API.
161
+
162
+ ```python
163
+ entry.field_by_id(12345).value = 'Updated text'
164
+ ```
165
+
166
+ > This is different from `field.set_value()`, which only updates the local object (used when building a submission).
167
+
168
+ ---
169
+
170
+ ## Submitting Forms
171
+
172
+ ### Simple submission with a field value dict
173
+
174
+ Keys are field IDs; values are converted to strings automatically.
175
+
176
+ ```python
177
+ ref_num = FormEntry.submit(auth, form_id='lqh', field_values={
178
+ 122619: 'Jane Doe',
179
+ 122620: 'jane@example.com',
180
+ 122621: 'Hello, thanks for your help.',
181
+ })
182
+ print(f'Submitted: {ref_num}')
183
+ ```
184
+
185
+ ### Submission using Field objects
186
+
187
+ Use the `Form` class to look up fields by name, then submit:
188
+
189
+ ```python
190
+ from formsmarts import APIAuthenticator, FormEntry, Form
191
+
192
+ auth = APIAuthenticator('FSA-999999', 'your-api-key')
193
+ form = Form(auth, 'lqh')
194
+
195
+ fields = []
196
+ for name, value in {'First Name': 'Jane', 'Last Name': 'Doe'}.items():
197
+ field = form.fields_by_name(name)[0]
198
+ field.set_value(value)
199
+ fields.append(field)
200
+
201
+ ref_num = FormEntry.submit(auth, 'lqh', fields)
202
+ ```
203
+
204
+ ### Submission with a file attachment
205
+
206
+ ```python
207
+ from formsmarts import Upload
208
+
209
+ FORM_ID = 'lqh'
210
+ UPLOAD_FIELD_ID = 122621
211
+
212
+ # Step 1: upload the file (accepts a path string or a file-like object)
213
+ upload = Upload.upload(
214
+ auth,
215
+ form_id=FORM_ID,
216
+ field_id=UPLOAD_FIELD_ID,
217
+ io_stream='/path/to/report.pdf',
218
+ filename='report.pdf',
219
+ )
220
+
221
+ # Step 2: submit the form, passing the Upload object as the field value
222
+ ref_num = FormEntry.submit(auth, FORM_ID, {
223
+ 122619: 'Jane Doe',
224
+ 122620: 'jane@example.com',
225
+ UPLOAD_FIELD_ID: upload.value, # the upload ID
226
+ })
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Attachments & PDFs
232
+
233
+ ### Download an attachment
234
+
235
+ ```python
236
+ upload_field = entry.fields_by_type('upload')[0]
237
+
238
+ # To a file path:
239
+ upload_field.download(f'/downloads/{upload_field.filename}')
240
+
241
+ # Or to a file-like object:
242
+ upload_field.download(open('copy.pdf', 'wb'))
243
+ ```
244
+
245
+ ### Replace an attachment
246
+
247
+ ```python
248
+ upload_field.replace('/path/to/new-file.pdf', filename='new-file.pdf')
249
+ ```
250
+
251
+ ### Download an entry as a PDF
252
+
253
+ ```python
254
+ FormEntry.download_pdf(auth, ref_num='ABC-123456', io_stream='/downloads/entry.pdf')
255
+
256
+ # With timezone localisation:
257
+ FormEntry.download_pdf(auth, ref_num='ABC-123456', io_stream='/downloads/entry.pdf', timezone='Europe/London')
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Tags
263
+
264
+ ```python
265
+ # Read tags
266
+ print(entry.tags) # user-defined tags
267
+ print(entry.system_tags) # tags assigned by FormSmarts
268
+
269
+ # Add and remove tags
270
+ entry.add_tag('reviewed')
271
+ entry.add_tags(['approved', 'priority'])
272
+ entry.remove_tag('reviewed')
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Sharing an Entry
278
+
279
+ Send a copy of a form submission by email:
280
+
281
+ ```python
282
+ entry.share('manager@example.com')
283
+
284
+ # Multiple recipients, with an optional note:
285
+ entry.share(
286
+ ['alice@example.com', 'bob@example.com'],
287
+ note='Please review this submission.',
288
+ sign_note=True,
289
+ )
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Pre-filling Forms
295
+
296
+ Generate a signed URL that opens the form with fields pre-populated. Optionally lock fields to prevent the user from changing them.
297
+
298
+ ```python
299
+ from formsmarts import APIAuthenticator, PreFilledForm
300
+ import os
301
+
302
+ auth = APIAuthenticator(os.environ['FS_ACCOUNT_ID'], os.environ['FS_API_KEY'])
303
+ pf = PreFilledForm(auth, form_id='lqh', prefill_key=os.environ['FS_PREFILL_KEY'])
304
+
305
+ data = {
306
+ 'Client ID': {'value': 789034, 'readonly': True},
307
+ 'First Name': {'value': 'Jane', 'readonly': True},
308
+ 'Last Name': {'value': 'Doe', 'readonly': True},
309
+ 'Email': {'value': 'jane@example.com'},
310
+ }
311
+
312
+ for name, opts in data.items():
313
+ fields = pf.fields_by_name(name)
314
+ if fields:
315
+ pf.pre_fill(fields[0], opts['value'], readonly=opts.get('readonly', False))
316
+
317
+ url = pf.get_url()
318
+ print(url)
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Webhooks
324
+
325
+ ### Verifying a webhook callback
326
+
327
+ Always verify that incoming requests originate from FormSmarts before processing them.
328
+
329
+ ```python
330
+ from formsmarts import WebhookAuthenticator, FormEntry, AuthenticationError
331
+ import json, os
332
+
333
+ wh_auth = WebhookAuthenticator(os.environ['FS_WEBHOOK_KEY'])
334
+
335
+ def handle_webhook(headers, body):
336
+ try:
337
+ wh_auth.verify_request(headers['Authorization'])
338
+ except AuthenticationError:
339
+ return 403
340
+
341
+ entry = FormEntry.create(json.loads(body))
342
+
343
+ for field in entry.fields:
344
+ print(field.name, field.value)
345
+
346
+ return 200
347
+ ```
348
+
349
+ ### Accessing webhook-only properties
350
+
351
+ ```python
352
+ print(entry.submitted_at) # datetime the form was submitted
353
+ print(entry.amount_due) # for forms with deferred payment
354
+ print(entry.is_validation_hook) # True for validation webhooks
355
+ ```
356
+
357
+ ### Downloading attachments from a webhook
358
+
359
+ Webhook entries include presigned URLs (valid for 5 minutes) for direct attachment access, in addition to the standard `download()` method which requires API authentication.
360
+
361
+ ```python
362
+ upload_field = entry.fields_by_type('upload')[0]
363
+ print(upload_field.presigned_url) # direct download URL, no auth needed
364
+ upload_field.download('/tmp/attachment.pdf') # uses API auth
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Concurrency
370
+
371
+ The client is synchronous. For workloads that require parallel API calls — such as fetching entries for a dashboard or processing a large batch — use `concurrent.futures.ThreadPoolExecutor`:
372
+
373
+ ```python
374
+ from concurrent.futures import ThreadPoolExecutor, as_completed
375
+ from formsmarts import FormEntry
376
+
377
+ entries = list(FormEntry.search_by_dates(auth, form_id='lqh', start_date='2025-01-01'))
378
+
379
+ with ThreadPoolExecutor(max_workers=5) as executor:
380
+ futures = {
381
+ executor.submit(
382
+ FormEntry.download_pdf, auth, entry.reference_number,
383
+ f'/downloads/{entry.reference_number}.pdf'
384
+ ): entry
385
+ for entry in entries
386
+ }
387
+ for future in as_completed(futures):
388
+ future.result() # re-raises any exception from the worker
389
+ ```
390
+
391
+ The client handles HTTP 429 (rate limit) responses automatically with exponential backoff. The default is 3 retries; pass `max_retries` to the underlying request if you need to adjust this.
392
+
393
+ ---
394
+
395
+ ## Error Handling
396
+
397
+ ```python
398
+ from formsmarts import APIRequestError, AuthenticationError
399
+
400
+ try:
401
+ entry = FormEntry.fetch(auth, ref_num='INVALID')
402
+ except AuthenticationError as e:
403
+ print('Auth failed:', e)
404
+ except APIRequestError as e:
405
+ print(f'API error {e.status}: {e.text}')
406
+ ```
407
+
408
+ | Exception | When raised |
409
+ |---|---|
410
+ | `AuthenticationError` | Invalid credentials or expired/malformed JWT |
411
+ | `APIRequestError` | Non-200 HTTP response from the API; exposes `.status`, `.text`, `.json` |
412
+ | `APIError` | Base class for all client errors |
413
+
414
+ ---
415
+
416
+ ## Links
417
+
418
+ - [FormSmarts API & Webhook documentation](https://formsmarts.com/api-webhook-client)
419
+ - [Form Submission API](https://formsmarts.com/form-submission-api)
420
+ - [Form Response API](https://formsmarts.com/form-response-api)
421
+ - [Webhooks](https://formsmarts.com/online-form-webhook)
422
+ - [Pre-populate a form](https://formsmarts.com/pre-populate-a-form)
423
+ - [Account settings](https://formsmarts.com/account/view)
424
+
425
+ ## License
426
+
427
+ Copyright © Syronex LLC. All rights reserved.