python-anvil 5.1.2__tar.gz → 6.0.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.
Files changed (60) hide show
  1. python_anvil-6.0.0/PKG-INFO +751 -0
  2. python_anvil-6.0.0/README.md +716 -0
  3. python_anvil-6.0.0/pyproject.toml +149 -0
  4. python_anvil-6.0.0/python_anvil/__init__.py +26 -0
  5. python_anvil-6.0.0/python_anvil/api.py +698 -0
  6. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/base.py +1 -4
  7. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/__init__.py +8 -0
  8. python_anvil-6.0.0/python_anvil/api_resources/mutations/base.py +33 -0
  9. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/create_etch_packet.py +63 -108
  10. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/forge_submit.py +22 -69
  11. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/generate_etch_signing_url.py +8 -8
  12. python_anvil-6.0.0/python_anvil/api_resources/mutations/helpers.py +5 -0
  13. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/payload.py +86 -103
  14. python_anvil-6.0.0/python_anvil/auth.py +60 -0
  15. python_anvil-6.0.0/python_anvil/cli.py +326 -0
  16. python_anvil-6.0.0/python_anvil/constants.py +43 -0
  17. python_anvil-6.0.0/python_anvil/errors.py +132 -0
  18. python_anvil-6.0.0/python_anvil/exceptions.py +9 -0
  19. python_anvil-6.0.0/python_anvil/graphql/__init__.py +11 -0
  20. python_anvil-6.0.0/python_anvil/graphql/mutations/__init__.py +14 -0
  21. python_anvil-6.0.0/python_anvil/graphql/mutations/create_etch_packet.py +79 -0
  22. python_anvil-6.0.0/python_anvil/graphql/mutations/forge_submit.py +60 -0
  23. python_anvil-6.0.0/python_anvil/graphql/mutations/generate_etch_sign_url.py +12 -0
  24. python_anvil-6.0.0/python_anvil/graphql/mutations/remove_weld_data.py +9 -0
  25. python_anvil-6.0.0/python_anvil/graphql/queries/__init__.py +4 -0
  26. python_anvil-6.0.0/python_anvil/graphql/queries/etch_packet.py +45 -0
  27. python_anvil-6.0.0/python_anvil/graphql/selection.py +15 -0
  28. python_anvil-6.0.0/python_anvil/http.py +316 -0
  29. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/models.py +40 -18
  30. python_anvil-6.0.0/python_anvil/rate_limit.py +272 -0
  31. python_anvil-6.0.0/python_anvil/types/__init__.py +58 -0
  32. python_anvil-6.0.0/python_anvil/types/_base.py +21 -0
  33. python_anvil-6.0.0/python_anvil/types/graphql.py +2626 -0
  34. python_anvil-6.0.0/python_anvil/types/operations.py +1261 -0
  35. python_anvil-6.0.0/python_anvil/types/rest.py +700 -0
  36. python_anvil-6.0.0/python_anvil/upload.py +269 -0
  37. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/utils.py +6 -1
  38. python_anvil-5.1.2/PKG-INFO +0 -77
  39. python_anvil-5.1.2/README.md +0 -43
  40. python_anvil-5.1.2/pyproject.toml +0 -103
  41. python_anvil-5.1.2/python_anvil/__init__.py +0 -12
  42. python_anvil-5.1.2/python_anvil/api.py +0 -447
  43. python_anvil-5.1.2/python_anvil/api_resources/mutations/base.py +0 -20
  44. python_anvil-5.1.2/python_anvil/api_resources/mutations/helpers.py +0 -7
  45. python_anvil-5.1.2/python_anvil/api_resources/requests.py +0 -183
  46. python_anvil-5.1.2/python_anvil/cli.py +0 -363
  47. python_anvil-5.1.2/python_anvil/constants.py +0 -25
  48. python_anvil-5.1.2/python_anvil/exceptions.py +0 -6
  49. python_anvil-5.1.2/python_anvil/http.py +0 -226
  50. python_anvil-5.1.2/python_anvil/tests/__init__.py +0 -1
  51. python_anvil-5.1.2/python_anvil/tests/conftest.py +0 -11
  52. python_anvil-5.1.2/python_anvil/tests/payloads.py +0 -92
  53. python_anvil-5.1.2/python_anvil/tests/test_api.py +0 -457
  54. python_anvil-5.1.2/python_anvil/tests/test_cli.py +0 -112
  55. python_anvil-5.1.2/python_anvil/tests/test_http.py +0 -145
  56. python_anvil-5.1.2/python_anvil/tests/test_models.py +0 -264
  57. python_anvil-5.1.2/python_anvil/tests/test_utils.py +0 -39
  58. {python_anvil-5.1.2 → python_anvil-6.0.0}/LICENSE.md +0 -0
  59. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/__init__.py +0 -0
  60. {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/py.typed +0 -0
@@ -0,0 +1,751 @@
1
+ Metadata-Version: 2.4
2
+ Name: python_anvil
3
+ Version: 6.0.0
4
+ Summary: Anvil API Client
5
+ License-Expression: MIT
6
+ License-File: LICENSE.md
7
+ Keywords: anvil,api,pdf,signing
8
+ Author: Anvil Foundry Inc.
9
+ Author-email: developers@useanvil.com
10
+ Requires-Python: >=3.10,<3.16
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Provides-Extra: cli
24
+ Requires-Dist: click (>=8.0,<9) ; extra == "cli"
25
+ Requires-Dist: pydantic (>=2.8,<3)
26
+ Requires-Dist: requests (>=2.28.2,<3)
27
+ Requires-Dist: tabulate (>=0.9,<0.11) ; extra == "cli"
28
+ Requires-Dist: typing-extensions (>=4.12)
29
+ Project-URL: Changelog, https://github.com/anvilco/python-anvil/blob/master/CHANGELOG.md
30
+ Project-URL: Documentation, https://github.com/anvilco/python-anvil#readme
31
+ Project-URL: Homepage, https://www.useanvil.com/
32
+ Project-URL: Repository, https://github.com/anvilco/python-anvil
33
+ Description-Content-Type: text/markdown
34
+
35
+ ![Horizontal Lockupblack](https://user-images.githubusercontent.com/293079/169453889-ae211c6c-7634-4ccd-8ca9-8970c2621b6f.png#gh-light-mode-only)
36
+ ![Horizontal Lockup copywhite](https://user-images.githubusercontent.com/293079/169453892-895f637b-4633-4a14-b997-960c9e17579b.png#gh-dark-mode-only)
37
+
38
+ # Anvil API Client for Python
39
+
40
+ [![PyPI Version](https://img.shields.io/pypi/v/python-anvil.svg)](https://pypi.org/project/python-anvil)
41
+ [![PyPI License](https://img.shields.io/pypi/l/python-anvil.svg)](https://pypi.org/project/python-anvil)
42
+
43
+ This library will allow you to interact with the [Anvil API](https://www.useanvil.com/developers) in Python.
44
+
45
+ [Anvil](https://www.useanvil.com/developers/) provides easy APIs for all things paperwork.
46
+
47
+ 1. [PDF filling API](https://www.useanvil.com/products/pdf-filling-api/) - fill out a PDF template with a web request and structured JSON data.
48
+ 2. [PDF generation API](https://www.useanvil.com/products/pdf-generation-api/) - send markdown or HTML and Anvil will render it to a PDF.
49
+ 3. [Etch e-sign with API](https://www.useanvil.com/products/etch/) - customizable, embeddable, e-signature platform with an API to control the signing process end-to-end.
50
+ 4. [Anvil Workflows (w/ API)](https://www.useanvil.com/products/workflows/) - Webforms + PDF + e-sign with a powerful no-code builder. Easily collect structured data, generate PDFs, and request signatures.
51
+
52
+ Learn more on our [Anvil developer page](https://www.useanvil.com/developers/). See the [API guide](https://www.useanvil.com/docs) and the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for full documentation.
53
+
54
+ ## Usage
55
+
56
+ Requires Python 3.10 or later.
57
+
58
+ ```sh
59
+ pip install python-anvil
60
+ ```
61
+
62
+ Or with Poetry, uv or another tool, add the `python-anvil` package. The `anvil` command line tool is an optional extra:
63
+
64
+ ```sh
65
+ pip install 'python-anvil[cli]'
66
+ ```
67
+
68
+ A basic example converting your JSON to a filled PDF, then saving the PDF to a file:
69
+
70
+ ```python
71
+ from python_anvil import Anvil
72
+
73
+ # The ID of the PDF template to fill
74
+ pdf_template_id = "kA6Da9CuGqUtc6QiBDRR"
75
+ # Your API key from your Anvil organization settings
76
+ api_key = "7j2JuUWmN4fGjBxsCltWaybHOEy3UEtt"
77
+
78
+ # JSON data to fill the PDF
79
+ example_data = {
80
+ "title": "My PDF Title",
81
+ "fontSize": 10,
82
+ "textColor": "#CC0000",
83
+ "data": {
84
+ "someFieldId": "Hello World!",
85
+ },
86
+ }
87
+ anvil = Anvil(api_key)
88
+ pdf_bytes = anvil.fill_pdf(pdf_template_id, example_data)
89
+
90
+ # By default, if the PDF has been published then the published version is what will
91
+ # be filled. If the PDF has not been published, then the most recent version will
92
+ # be filled.
93
+ #
94
+ # However, a version number can also be passed in that will be used to retrieve and
95
+ # fill a specific version of the PDF.
96
+ # You can also use the constant `Anvil.VERSION_LATEST` (or `-1`) to fill the most
97
+ # recent version of your PDF, whether that version has been published or not.
98
+ # Use this if you'd like to fill out a draft version of your template/PDF.
99
+ pdf_bytes = anvil.fill_pdf(pdf_template_id, example_data, version_number=Anvil.VERSION_LATEST)
100
+
101
+ # The filled PDF's raw bytes
102
+ with open("output.pdf", "wb") as f:
103
+ f.write(pdf_bytes)
104
+ ```
105
+
106
+ ### Types
107
+
108
+ The client's request types are generated from the Anvil API's GraphQL schema and OpenAPI spec, as [pydantic](https://docs.pydantic.dev/) models in `python_anvil.types`, the Python counterpart of [`@anvilco/types`](https://www.npmjs.com/package/@anvilco/types). Field names are snake_case with camelCase aliases, and either works. `FillPDFPayload`, `GeneratePDFPayload`, `EtchSigner`, `CreateEtchPacketPayload` and `ForgeSubmitPayload` extend them, so every option the API documents is available, and a value the API doesn't accept, like an unknown enum value, is a validation error before anything is sent:
109
+
110
+ ```python
111
+ from python_anvil.api_resources.payload import FillPDFPayload
112
+
113
+ payload = FillPDFPayload(data={"someFieldId": "Hello"}, font_size=10, use_interactive_fields=True)
114
+ pdf_bytes = anvil.fill_pdf(pdf_template_id, payload)
115
+ ```
116
+
117
+ * `python_anvil.types.rest`: the REST request bodies, like `FillPdfRequest`, `GenerateMarkdownPdfRequest` and `EtchSigner`.
118
+ * `python_anvil.types.graphql`: every type in the GraphQL schema, by name: objects like `EtchPacket`, inputs like `EtchFile`.
119
+ * `python_anvil.types.operations`: the arguments of every query and mutation, like `CreateEtchPacketArgs` and `ForgeSubmitArgs`.
120
+
121
+ A plain `dict` works everywhere a model does, and extra fields are kept, so a feature the API added that this library doesn't know about yet still gets sent.
122
+
123
+ ## API
124
+
125
+ * [Constructor](#anvilapi_key-options)
126
+ * [fill_pdf(template_id, payload, ...)](#fill_pdftemplate_id-payload-version_number-data_type)
127
+ * [generate_pdf(payload, ...)](#generate_pdfpayload-data_type)
128
+ * [create_etch_packet(payload, ...)](#create_etch_packetpayload-json-response_query-mutation)
129
+ * [get_etch_packet(eid, ...)](#get_etch_packeteid-response_query)
130
+ * [generate_etch_signing_url(signer_eid, client_user_id)](#generate_etch_signing_urlsigner_eid-client_user_id)
131
+ * [download_documents(document_group_eid, ...)](#download_documentsdocument_group_eid-data_type)
132
+ * [forge_submit(payload, ...)](#forge_submitpayload-json-response_query-mutation)
133
+ * [remove_weld_data(eid)](#remove_weld_dataeid-mutation)
134
+ * [request_graphql(query, variables)](#request_graphqlquery-variables)
135
+ * [request_rest(method, path, ...)](#request_restmethod-path-json-params-data_type)
136
+ * [Anvil.prepare_graphql_file(path_or_file, ...)](#anvilprepare_graphql_filepath_or_file-filename-mimetype)
137
+ * [Other queries](#other-queries)
138
+ * [Options](#options), including [authentication](#authentication) and [base_url](#base_url)
139
+ * [Constants](#constants)
140
+ * [Errors](#errors)
141
+ * [Rate limits](#rate-limits)
142
+ * [Retries](#retries)
143
+ * [Command line tool](#command-line-tool)
144
+
145
+ ### Instance methods
146
+
147
+ ##### Anvil(api_key, **options)
148
+
149
+ Creates an Anvil client instance.
150
+
151
+ * `api_key` (str) - Your API key. Read from the `ANVIL_API_KEY` environment variable when left out. Or pass `access_token=` instead; see [Authentication](#authentication).
152
+ * `**options` - [Options](#options) for the client, as keyword arguments.
153
+
154
+ ```python
155
+ from python_anvil import Anvil
156
+
157
+ anvil = Anvil("abc123")
158
+ # Or, with ANVIL_API_KEY set in the environment
159
+ anvil = Anvil()
160
+ ```
161
+
162
+ Every method also takes a `timeout` keyword argument that overrides the client's [`timeout`](#options) for that one request.
163
+
164
+ The client can also be used as a context manager, which closes its HTTP session when done.
165
+
166
+ ##### fill_pdf(template_id, payload[, version_number, data_type])
167
+
168
+ Fills a PDF template with your JSON data.
169
+
170
+ First, you will need to have [uploaded a PDF to Anvil](https://useanvil.com/docs/api/fill-pdf#creating-a-pdf-template). You can find the PDF template's ID on the PDF template's page. If you already have the URL to the page in Anvil, the ID is there as well: `https://app.useanvil.com/org/<your-org-slug>/pdf/<pdf-template-id>`
171
+
172
+ ```python
173
+ # PDF template you uploaded to Anvil
174
+ pdf_template_id = "kA6Da9CuGqUtc6QiBDRR"
175
+
176
+ # JSON data to fill the PDF
177
+ payload = {
178
+ "title": "My PDF Title",
179
+ "fontSize": 10,
180
+ "textColor": "#CC0000",
181
+ "data": {
182
+ "someFieldId": "Hello World!",
183
+ },
184
+ }
185
+ pdf_bytes = anvil.fill_pdf(pdf_template_id, payload)
186
+
187
+ # Be sure to write the file as raw bytes
188
+ with open("filled.pdf", "wb") as f:
189
+ f.write(pdf_bytes)
190
+ ```
191
+
192
+ * `template_id` (str) - The id of your PDF template from the Anvil UI
193
+ * `payload` (dict | str | `FillPDFPayload`) - The JSON data that will fill the PDF template, as a dict, a JSON string, or a `FillPDFPayload`. Keys may be snake_case or camelCase.
194
+ * `title` (str) - _optional_ Set the title encoded into the PDF document
195
+ * `font_size` (int) - _optional_ Set the font size of all filled text. Default is 10.
196
+ * `text_color` (str) - _optional_ Set the text color of all filled text. Default is dark blue.
197
+ * `data` (dict) - The data to fill the PDF. The keys in this object will correspond to a field's ID in the PDF. These field IDs and their types are available on the PDF template's page in the Anvil dashboard.
198
+ * For example `{"someFieldId": "Hello World!"}`
199
+ * See `python_anvil.types.rest.FillPdfRequest` for every option.
200
+ * `version_number` (int) - _optional_ The version of the PDF template to fill. Defaults to the latest published version, or the latest version if the template has never been published. See [Constants](#constants).
201
+ * `data_type` (str) - _optional_ What to return. `"bytes"` (the default) returns the PDF's bytes; `"stream"` returns the `requests.Response`, to stream a large PDF with `iter_content`. See [request_rest](#request_restmethod-path-json-params-data_type).
202
+ * `params` (dict) - _optional_ Extra query string parameters.
203
+ * Returns the filled PDF's raw bytes, or raises an [`AnvilRequestError`](#errors).
204
+
205
+ ##### generate_pdf(payload[, data_type])
206
+
207
+ Dynamically generate a new PDF from your HTML and CSS or markdown.
208
+
209
+ Useful for agreements, invoices, disclosures, or any other text-heavy documents. This does not require you do anything in the Anvil UI other than setup your API key, just send it data, get a PDF. See [the generate PDF docs](https://useanvil.com/api/generate-pdf) for full details.
210
+
211
+ * [HTML to PDF docs](https://www.useanvil.com/docs/api/generate-pdf#html--css-to-pdf)
212
+ * [Markdown to PDF docs](https://www.useanvil.com/docs/api/generate-pdf#markdown-to-pdf)
213
+
214
+ Check out our [HTML invoice template](https://github.com/anvilco/html-pdf-invoice-template) for a complete HTML to PDF example.
215
+
216
+ ```python
217
+ # An example using an HTML to PDF payload
218
+ payload = {
219
+ "title": "Example",
220
+ "type": "html",
221
+ "data": {
222
+ "html": """
223
+ <h1 class='header-one'>What is Lorem Ipsum?</h1>
224
+ <p>
225
+ Lorem Ipsum is simply dummy text...
226
+ </p>
227
+ """,
228
+ "css": """
229
+ body { font-size: 14px; color: #171717; }
230
+ .header-one { text-decoration: underline; }
231
+ """,
232
+ },
233
+ }
234
+
235
+ # An example using a Markdown payload
236
+ payload = {
237
+ "title": "Example Invoice",
238
+ "data": [
239
+ {"label": "Name", "content": "Sally Jones"},
240
+ {"content": "Lorem **ipsum** dolor sit _amet_"},
241
+ {
242
+ "table": {
243
+ "firstRowHeaders": True,
244
+ "rows": [
245
+ ["Description", "Quantity", "Price"],
246
+ ["4x Large Widgets", "4", "$40.00"],
247
+ ["10x Medium Sized Widgets in dark blue", "10", "$100.00"],
248
+ ["10x Small Widgets in white", "6", "$60.00"],
249
+ ],
250
+ },
251
+ },
252
+ ],
253
+ }
254
+ pdf_bytes = anvil.generate_pdf(payload)
255
+
256
+ # Be sure to write the file as raw bytes
257
+ with open("generated.pdf", "wb") as f:
258
+ f.write(pdf_bytes)
259
+ ```
260
+
261
+ * `payload` (dict | str | `GeneratePDFPayload`) - The JSON data that will generate the PDF
262
+ * `title` (str) - _optional_ Set the title encoded into the PDF document
263
+ * `type` (str) - _optional_ `"markdown"` (the default) or `"html"`
264
+ * `data` (list | dict) - The data that generates the PDF: a list of content blocks for Markdown, or `{"html": ..., "css": ...}` for HTML. See [the docs](https://useanvil.com/docs/api/generate-pdf#supported-format-of-data) for all supported objects.
265
+ * `page`, `logo`, `font_size`, `font_family`, `text_color`, `include_timestamp` - _optional_ See `python_anvil.types.rest.GenerateMarkdownPdfRequest` for every option.
266
+ * `data_type` (str) - _optional_ `"bytes"` (the default) or `"stream"`, like [fill_pdf](#fill_pdftemplate_id-payload-version_number-data_type).
267
+ * `params` (dict) - _optional_ Extra query string parameters.
268
+ * Returns the generated PDF's raw bytes, or raises an [`AnvilRequestError`](#errors).
269
+
270
+ ##### create_etch_packet(payload[, json, response_query, mutation])
271
+
272
+ Creates an Etch Packet and optionally sends it to the first signer.
273
+
274
+ ```python
275
+ from python_anvil import Anvil
276
+
277
+ anvil = Anvil(api_key)
278
+
279
+ data = anvil.create_etch_packet({
280
+ "name": "Example packet",
281
+ # Test packets use development signatures and are not billed
282
+ "is_test": True,
283
+ # Send it to the first signer right away
284
+ "is_draft": False,
285
+ "files": [{
286
+ "id": "uploadedNDA",
287
+ "title": "Demo NDA",
288
+ "file": Anvil.prepare_graphql_file("./nda.pdf"),
289
+ "fields": [{
290
+ "id": "signatureOne",
291
+ "type": "signature",
292
+ "pageNum": 0,
293
+ "rect": {"x": 100, "y": 100, "width": 150, "height": 30},
294
+ }],
295
+ }],
296
+ "signers": [{
297
+ "id": "signerOne",
298
+ "name": "Sally Signer",
299
+ "email": "sally@example.com",
300
+ # 'embedded' signers sign in your app with a URL from generate_etch_signing_url.
301
+ # The default, 'email', sends them an email.
302
+ "signerType": "embedded",
303
+ "fields": [{"fileId": "uploadedNDA", "fieldId": "signatureOne"}],
304
+ }],
305
+ })
306
+
307
+ etch_packet = data["createEtchPacket"]
308
+ signer_eid = etch_packet["documentGroup"]["signers"][0]["eid"]
309
+ ```
310
+
311
+ * `payload` (dict | str | `CreateEtchPacket` | `CreateEtchPacketPayload`) - The mutation's variables, as a dict with snake_case or camelCase keys, a JSON string, the [builder](#the-createetchpacket-builder) or the model. A dict's `files` may be uploads, template references (`{"id": ..., "castEid": ...}`), Markdown or HTML documents, or `DocumentUpload`, `EtchCastRef`, `DocumentMarkdown` and `DocumentMarkup` instances. See the [API Documentation](#api-documentation) for details, and the [`CreateEtchPacket` builder](#the-createetchpacket-builder) below for building a packet step by step. Files to upload can be base64 dicts, or prepared with [prepare_graphql_file](#anvilprepare_graphql_filepath_or_file-filename-mimetype).
312
+ * `json` (str) - _optional_ The variables as a JSON string, instead of `payload`.
313
+ * `response_query` (str) - _optional_ The fields to select on the created packet, like `"{ eid status }"` (the braces are optional). Can be left out to use the default.
314
+ * `mutation` (str) - _optional_ If you'd like complete control of the GraphQL mutation, you can pass in a GraphQL Mutation compliant string that will be used in the mutation call. This string should also include your response query, as the `response_query` param is ignored if `mutation` is passed. Example:
315
+ ```graphql
316
+ mutation CreateEtchPacket (
317
+ $name: String,
318
+ ...
319
+ ) {
320
+ createEtchPacket (
321
+ name: $name,
322
+ ...
323
+ ) {
324
+ id
325
+ eid
326
+ ...
327
+ }
328
+ }
329
+ ```
330
+ * Returns the response's `data`, with the packet under `createEtchPacket`, or raises an [`AnvilRequestError`](#errors).
331
+
332
+ ###### The `CreateEtchPacket` builder
333
+
334
+ Instead of a dict, `create_etch_packet` takes a `CreateEtchPacket`, a builder that collects the packet's files and signers, generates signer IDs and routing orders, and lets you pre-fill the PDFs:
335
+
336
+ ```python
337
+ from python_anvil import Anvil
338
+ from python_anvil.api_resources.mutations import CreateEtchPacket
339
+ from python_anvil.api_resources.payload import (
340
+ DocumentUpload,
341
+ EtchCastRef,
342
+ EtchSigner,
343
+ FillPDFPayload,
344
+ SignatureField,
345
+ SignerField,
346
+ )
347
+
348
+ anvil = Anvil(api_key)
349
+
350
+ packet = CreateEtchPacket(
351
+ name="Packet Name",
352
+ signature_email_subject="Please sign these forms",
353
+ # Test packets use development signatures and are not billed
354
+ is_test=True,
355
+ )
356
+
357
+ # A PDF to upload. `prepare_graphql_file` takes a path, bytes or an open file.
358
+ packet.add_file(
359
+ DocumentUpload(
360
+ id="myNewFile",
361
+ title="Please sign this important form",
362
+ file=Anvil.prepare_graphql_file("./important-form.pdf"),
363
+ fields=[
364
+ SignatureField(
365
+ id="firstSignature",
366
+ type="signature",
367
+ page_num=0,
368
+ # The position and size of the field
369
+ rect={"x": 100, "y": 100, "width": 100, "height": 100},
370
+ )
371
+ ],
372
+ )
373
+ )
374
+
375
+ # Or a PDF template from your Anvil account. Find its eid under "API Info"
376
+ # on the template's page, or with the CLI: `anvil cast --list`
377
+ packet.add_file(EtchCastRef(id="fileAlias", cast_eid="CAST_EID_GOES_HERE"))
378
+
379
+ # Pre-fill the template, with the same payload shape `fill_pdf` takes
380
+ packet.add_file_payloads("fileAlias", FillPDFPayload(data={"aTextFieldId": "This is pre-filled."}))
381
+
382
+ packet.add_signer(
383
+ EtchSigner(
384
+ name="Jackie",
385
+ email="jackie@example.com",
386
+ # Which fields this signer signs: the file ids and field ids above
387
+ fields=[
388
+ SignerField(file_id="myNewFile", field_id="firstSignature"),
389
+ SignerField(file_id="fileAlias", field_id="signOne"),
390
+ ],
391
+ # "email" (the default) emails the signer; "embedded" doesn't, and you
392
+ # send them to a URL from `generate_etch_signing_url` instead
393
+ signer_type="email",
394
+ # "draw" lets the signer draw their signature; "text" types their name
395
+ signature_mode="draw",
396
+ # Where the signer is sent after signing
397
+ redirect_url="https://app.useanvil.com",
398
+ )
399
+ )
400
+
401
+ data = anvil.create_etch_packet(packet)
402
+ ```
403
+
404
+ `EtchSigner` extends the generated [types](#types), so every signer option the API has is available on it.
405
+
406
+ ##### get_etch_packet(eid[, response_query])
407
+
408
+ Gets the details of an Etch Packet.
409
+
410
+ ```python
411
+ packet = anvil.get_etch_packet(etch_packet_eid)
412
+ status, document_group = packet["status"], packet["documentGroup"]
413
+ ```
414
+
415
+ * `eid` (str) - your Etch Packet eid
416
+ * `response_query` (str) - _optional_ A GraphQL Query compliant query to use for the data desired in the query response. Can be left out to use default.
417
+ * Returns the packet, or raises an [`AnvilRequestError`](#errors).
418
+
419
+ ##### generate_etch_signing_url(signer_eid, client_user_id)
420
+
421
+ Generates an Etch sign URL for an Etch Packet signer. The Etch Packet and its signers must have already been created, the packet must have been sent, and the signer must be an `embedded` signer.
422
+
423
+ ```python
424
+ url = anvil.generate_etch_signing_url(
425
+ signer_eid,
426
+ # Your ID for the user who will be signing
427
+ client_user_id="user-123",
428
+ )
429
+ # Send your user to `url` to sign
430
+ ```
431
+
432
+ * `signer_eid` (str) - the eid of the Etch Packet signer, found in the response of the `create_etch_packet` instance method
433
+ * `client_user_id` (str) - your ID for the user who will sign
434
+ * Returns the signing URL, or raises an [`AnvilRequestError`](#errors).
435
+
436
+ Also available as `generate_etch_sign_url`, node-anvil's name for it.
437
+
438
+ ##### download_documents(document_group_eid[, data_type])
439
+
440
+ Returns the document group specified by the `document_group_eid` in Zip file format.
441
+
442
+ ```python
443
+ zip_bytes = anvil.download_documents(document_group_eid)
444
+ with open("documents.zip", "wb") as f:
445
+ f.write(zip_bytes)
446
+
447
+ # Or stream it
448
+ with anvil.download_documents(document_group_eid, data_type="stream") as response, open("documents.zip", "wb") as f:
449
+ for chunk in response.iter_content(chunk_size=64 * 1024):
450
+ f.write(chunk)
451
+ ```
452
+
453
+ * `document_group_eid` (str) - the eid of the document group to download
454
+ * `data_type` (str) - _optional_ `"bytes"` (the default) or `"stream"`, like [fill_pdf](#fill_pdftemplate_id-payload-version_number-data_type).
455
+ * Returns the zip file's raw bytes, or raises an [`AnvilRequestError`](#errors).
456
+
457
+ ##### forge_submit(payload[, json, response_query, mutation])
458
+
459
+ Creates or updates a submission to a webform in a Workflow, the same as someone filling in and submitting that webform. Webforms are called Forges in the API.
460
+
461
+ ```python
462
+ data = anvil.forge_submit({
463
+ # The webform's eid
464
+ "forge_eid": "your-forge-eid",
465
+ # Keys are the IDs of the webform's fields
466
+ "payload": {
467
+ "name": "Sally Jones",
468
+ "email": "sally@example.com",
469
+ },
470
+ "is_test": True,
471
+ })
472
+ submission = data["forgeSubmit"]
473
+ weld_data_eid = submission["weldData"]["eid"]
474
+ ```
475
+
476
+ * `payload` (dict | str | `ForgeSubmitPayload`) - The mutation's variables as a dict (snake_case or camelCase keys), a JSON string or the model. Requires `forge_eid` and `payload`. Pass `weld_data_eid` and `submission_eid` to update an existing submission. See the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for all variables.
477
+ * `json` (str) - _optional_ The variables as a JSON string, instead of `payload`.
478
+ * `response_query` (str) - _optional_ A GraphQL Query compliant query to use for the data desired in the mutation response. Can be left out to use default.
479
+ * `mutation` (str) - _optional_ Your own complete GraphQL mutation. `response_query` is ignored if this is passed.
480
+ * Returns the response's `data`, like [request_graphql](#request_graphqlquery-variables).
481
+
482
+ ##### remove_weld_data(eid[, mutation])
483
+
484
+ Deletes a Workflow submission (a `WeldData`) and everything in it.
485
+
486
+ ```python
487
+ removed = anvil.remove_weld_data(weld_data_eid) # True
488
+ ```
489
+
490
+ * `eid` (str) - the eid of the WeldData to remove
491
+ * `mutation` (str) - _optional_ Your own complete GraphQL mutation.
492
+ * Returns whether it was removed, or raises an [`AnvilRequestError`](#errors).
493
+
494
+ ##### request_graphql(query[, variables])
495
+
496
+ A fallback function for queries and mutations without a specialized function in this client.
497
+
498
+ See the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for a listing on all possible queries
499
+
500
+ ```python
501
+ data = anvil.request_graphql(
502
+ """
503
+ query WeldDataQuery ($eid: String!) {
504
+ weldData (eid: $eid) {
505
+ eid
506
+ isComplete
507
+ isTest
508
+ }
509
+ }
510
+ """,
511
+ {"eid": "nxflNZqxDUbltLUbYWK"},
512
+ )
513
+ weld_data = data["weldData"]
514
+ ```
515
+
516
+ * `query` (str) - GraphQL query or mutation to run. See the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for a listing on all possible queries
517
+ * `variables` (dict | str) - _optional_ GraphQL variables for the query, as a dict or a JSON string. Files to upload can be anywhere in here; see [prepare_graphql_file](#anvilprepare_graphql_filepath_or_file-filename-mimetype).
518
+ * `timeout` (float) - _optional_ Overrides the client's `timeout` for this request.
519
+ * Returns the response's `data`: use the query or mutation name to reference the data that you requested. Raises an [`AnvilRequestError`](#errors) for GraphQL errors (which come with a `200` status) and HTTP errors alike.
520
+
521
+ `query(query, variables)` and `mutate(query, variables)` are the same method, under the names earlier versions used.
522
+
523
+ ##### request_rest(method, path[, json, params, data_type])
524
+
525
+ A fallback function for REST endpoints without a specialized function in this client.
526
+
527
+ ```python
528
+ pdf_bytes = anvil.request_rest(
529
+ "POST",
530
+ f"/api/v1/fill/{pdf_template_id}.pdf",
531
+ json=payload,
532
+ data_type="bytes",
533
+ )
534
+ ```
535
+
536
+ * `method` (str) - The HTTP method, e.g. `"POST"`
537
+ * `path` (str) - Path from the `base_url`, e.g. `/api/v1/fill/abc.pdf`
538
+ * `json` (Any) - _optional_ A JSON body
539
+ * `params` (dict) - _optional_ Query string parameters
540
+ * `data` (Any) - _optional_ A raw body, e.g. bytes or a file object, instead of `json`
541
+ * `headers` (dict) - _optional_ Extra headers
542
+ * `data_type` (str) - _optional_ What to return. `"json"` (the default) returns the parsed JSON, `"bytes"` the raw body, and `"stream"` the [`requests.Response`](https://requests.readthedocs.io/en/latest/api/#requests.Response) itself, to stream a large body with `iter_content`. Close it when done, or use it as a context manager.
543
+ * `timeout` (float) - _optional_ Overrides the client's `timeout` for this request
544
+ * Returns the data, or raises an [`AnvilRequestError`](#errors).
545
+
546
+ ### Class methods
547
+
548
+ ##### Anvil.prepare_graphql_file(path_or_file[, filename, mimetype])
549
+
550
+ Prepares a file for a GraphQL multipart upload, e.g. in `create_etch_packet`'s `files`. A file can be anywhere in a mutation's variables, under a `file` key. Also available as `python_anvil.prepare_graphql_file`.
551
+
552
+ ```python
553
+ # A path. The file is opened when the request is sent.
554
+ file = Anvil.prepare_graphql_file("./nda.pdf")
555
+
556
+ # Bytes, which need a filename
557
+ file = Anvil.prepare_graphql_file(pdf_bytes, filename="nda.pdf")
558
+
559
+ # An open binary file object. One with a name, like an open file, is sent with it.
560
+ with open("./nda.pdf", "rb") as f:
561
+ file = Anvil.prepare_graphql_file(f)
562
+ data = anvil.create_etch_packet({..., "files": [{"id": "nda", "title": "NDA", "file": file, "fields": [...]}]})
563
+ ```
564
+
565
+ * `path_or_file` (str | Path | bytes | file object) - The file to upload
566
+ * `filename` (str) - _optional_ The filename to send. Required for `bytes` and for file objects with no `name`, like a `BytesIO`.
567
+ * `mimetype` (str) - _optional_ The Content-Type to send the file with. Defaults to one guessed from the filename.
568
+ * `ignore_filename_validation` (bool) - _optional_ Send a file without a filename, as `blob`. An escape hatch for things we didn't anticipate.
569
+ * Returns an `UploadWithOptions`, which the client recognizes in the variables.
570
+
571
+ A request that gets rate limited is retried: a file given as a path is opened again, and a file object is rewound. A file object that can't be read again fails with an error rather than sending an empty file. Files can also be passed as a base64 dict, `{"data": "...", "filename": "nda.pdf", "mimetype": "application/pdf"}`, which is sent as plain JSON; `DocumentUpload` encodes an open file this way unless it was prepared with `prepare_graphql_file`.
572
+
573
+ ### Other queries
574
+
575
+ A few queries have methods of their own, for exploring your account. They return the query's data directly.
576
+
577
+ * `get_current_user()` - The user the API key belongs to, with their organizations.
578
+ * `get_casts(fields=None, show_all=False)` - The PDF templates (casts) in every organization, up to 500 each. By default their `eid`, `title` and `fieldInfo`; `show_all` includes casts that aren't templates.
579
+ * `get_cast(eid, fields=None, version_number=None, cast_args=None)` - One PDF template. `cast_args` are extra `(name, value)` arguments for the `cast` query, with the value in GraphQL syntax.
580
+ * `get_welds()` - The workflows (welds) in every organization, with their webforms (forges), up to 500 each.
581
+ * `get_weld(eid)` - One workflow, with its webforms.
582
+
583
+ ### Options
584
+
585
+ Options for the Anvil client, as keyword arguments. Defaults are shown after each option.
586
+
587
+ ```python
588
+ Anvil(
589
+ # One of api_key or access_token is required. See Authentication.
590
+ api_key="<your_api_key>",
591
+ access_token="<oauth_access_token>",
592
+
593
+ # Where to send requests. See base_url.
594
+ base_url="https://app.useanvil.com",
595
+
596
+ # Sent as the User-Agent header
597
+ user_agent="python-anvil/<version>",
598
+
599
+ # How many requests to allow per request_limit_ms until the first response from
600
+ # the API tells the client your actual limits. See Rate limits.
601
+ request_limit=1,
602
+ request_limit_ms=1000,
603
+
604
+ # Seconds to wait for a connection, and then for each read, before a request
605
+ # fails with a requests.Timeout. A (connect, read) tuple works too; None waits forever.
606
+ timeout=300,
607
+
608
+ # How many times to retry a request that got a 500, 502, 503 or 504, or a
609
+ # connection error. See Retries. 0 turns retries off.
610
+ max_retries=5,
611
+
612
+ # A requests.Session to send requests with, e.g. one configured with a proxy.
613
+ # Defaults to a session of the client's own.
614
+ session=None,
615
+ )
616
+ ```
617
+
618
+ ##### Authentication
619
+
620
+ Authenticate with either an API key or an OAuth access token.
621
+
622
+ **API key:** Find your API keys in your Anvil organization's settings. Requests use HTTP Basic auth: `Basic base64(api_key + ':')`.
623
+
624
+ ```python
625
+ anvil = Anvil(api_key="your-api-key")
626
+ ```
627
+
628
+ **OAuth access token:** Requests use Bearer auth. Anvil's access tokens are JWTs, which are sent as-is: `Bearer <access_token>`. Any other kind of token is base64 encoded: `Bearer base64(access_token)`.
629
+
630
+ ```python
631
+ anvil = Anvil(access_token="your-oauth-access-token")
632
+ ```
633
+
634
+ If you pass both, the `access_token` is used.
635
+
636
+ ##### base_url
637
+
638
+ By default, requests go to production, `https://app.useanvil.com`. Set `base_url` to send them somewhere else. All requests go to that host:
639
+
640
+ * REST: `{base_url}/api/v1/...`
641
+ * GraphQL: `{base_url}/graphql`
642
+ * Document downloads: `{base_url}/api/document-group/{eid}.zip`
643
+
644
+ ```python
645
+ anvil = Anvil(
646
+ api_key="your-api-key",
647
+ base_url="https://anvil.example.com",
648
+ )
649
+ ```
650
+
651
+ ### Constants
652
+
653
+ * `Anvil.VERSION_LATEST` (`-1`) - With `fill_pdf`'s `version_number` option, fills the most recent version of a PDF template, published or not. Use it to fill drafts.
654
+ * `Anvil.VERSION_LATEST_PUBLISHED` (`-2`) - Fills the latest published version. Unlike leaving `version_number` out, this returns an error if the template has never been published.
655
+
656
+ Both are also importable from `python_anvil`.
657
+
658
+ ### Errors
659
+
660
+ When the API responds with an error, the client's methods raise an `AnvilRequestError`. It has the `status_code`, the response `headers`, any partial `data` a GraphQL response came with, and `errors`: a list of dicts that each have a `message`, a `name`, or both.
661
+
662
+ ```python
663
+ from python_anvil import Anvil, AnvilRequestError
664
+
665
+ try:
666
+ pdf_bytes = anvil.fill_pdf(pdf_template_id, payload)
667
+ except AnvilRequestError as e:
668
+ print(e.status_code, e.errors)
669
+ print(e) # e.g. "400: ValidationError (data: Required)"
670
+ ```
671
+
672
+ `errors` looks like one of these:
673
+
674
+ * **GraphQL errors**, as returned by the API, often with a `200` `status_code`:
675
+ ```python
676
+ [{"message": "Not Found", "path": ["etchPacket"], "locations": [{"line": 2, "column": 3}]}]
677
+ ```
678
+ * **A REST error.** Validation errors have no `message`, but list each problem field:
679
+ ```python
680
+ [{"name": "AssertionError", "message": "PDF did not generate properly from given HTML!"}]
681
+ [{"name": "ValidationError", "status": 400, "fields": [{"message": "Required", "property": "data"}]}]
682
+ ```
683
+ * **A response that isn't JSON**, like a `502` from a proxy. The `name` and `message` are the HTTP status text, and `body` the start of the response:
684
+ ```python
685
+ [{"name": "Bad Gateway", "message": "Bad Gateway", "body": "<html>..."}]
686
+ ```
687
+ * **A JSON response that couldn't be parsed.** The parsing error's `name` and `message`:
688
+ ```python
689
+ [{"name": "JSONDecodeError", "message": "Expecting value: line 1 column 1 (char 0)"}]
690
+ ```
691
+
692
+ When a request can't be made at all, e.g. because of a network error or a timeout, the methods raise the [`requests` exception](https://requests.readthedocs.io/en/latest/api/#exceptions), such as `requests.ConnectionError` or `requests.Timeout`. Invalid arguments, like an unsupported `data_type` or a payload that fails validation, raise a `ValueError` (pydantic's `ValidationError` is one) before anything is sent.
693
+
694
+ `AnvilRequestError` and its base class `AnvilError` are also importable under their old names, `AnvilRequestException` and `AnvilException`, from `python_anvil.exceptions`.
695
+
696
+ ### Rate limits
697
+
698
+ Our API has request rate limits in place. The initial request made by this client will parse the limits for your account from the `X-RateLimit-Limit` and `X-RateLimit-Interval-Ms` response headers, and then handle the throttling of subsequent requests for you automatically. Any requests made while waiting for that first response will wait for it before being sent.
699
+
700
+ The limit is your account's, so every client in the process with the same `base_url` and credentials shares one limiter, and the limits it learned. An app that makes a new client for each request is limited as a whole, and the client is thread-safe, so requests from several threads share the limit too. The `request_limit` and `request_limit_ms` options are the starting limits until the first response, and only apply to the first client made for those credentials. Separate processes, like web server workers, each learn the limits for themselves.
701
+
702
+ Like the API, the client counts requests over a sliding window: it only holds a request back when `X-RateLimit-Limit` requests have already been sent in the last `X-RateLimit-Interval-Ms`. A request counts from when it is sent until an interval after its response arrives. The API counts from when the request reached it, which is somewhere in between, so it never sees more requests in its window than the client allowed.
703
+
704
+ In the event that this client still receives a `429 Too Many Requests` error response, it will wait for the number of seconds in the `Retry-After` header, plus 50ms, then retry the request. Without a `Retry-After` it waits a second.
705
+
706
+ See the [Anvil API docs](https://www.useanvil.com/docs/api/getting-started/#rate-limits) for more information on the specifics of the rate limits.
707
+
708
+ ### Retries
709
+
710
+ A request that gets a `500`, `502`, `503` or `504`, or that can't connect, is retried up to `max_retries` times (5 by default) with exponential backoff: 100ms before the first retry, then 200ms, 400ms, and so on, or the response's `Retry-After` when it has one, capped at a minute. Retries wait for the rate limit like any request, and an upload is sent in full again. Mutations are retried as well, so a request the API processed but answered with a `5xx` could be sent twice; pass `max_retries=0` to turn retries off for a client that must not do that. A request that still fails raises an [`AnvilRequestError`](#errors) with the last response, or the `requests.ConnectionError`.
711
+
712
+ ## Command line tool
713
+
714
+ The `anvil` command, installed with the `cli` extra, is a quick way to try the API from a shell. It uses the `ANVIL_API_KEY` environment variable, and `ANVIL_BASE_URL` to send requests somewhere other than production. Add `--help` after a command for its options.
715
+
716
+ ```sh
717
+ $ pip install 'python-anvil[cli]'
718
+ $ export ANVIL_API_KEY=MY_GENERATED_KEY
719
+
720
+ $ anvil
721
+ Usage: anvil [OPTIONS] COMMAND [ARGS]...
722
+
723
+ Commands:
724
+ cast Fetch Cast data given a Cast eid.
725
+ create-etch Create an etch packet with a JSON file.
726
+ current-user Show details about your API user
727
+ download-documents Download etch documents
728
+ fill-pdf Fill PDF template with data, once per row of a CSV file.
729
+ generate-etch-url Generate an etch url for a signer
730
+ generate-pdf Generate a PDF
731
+ gql-query Run a raw graphql query
732
+ weld Fetch weld info or list of welds
733
+
734
+ # Fill a sample PDF template with each row of a CSV file
735
+ $ anvil fill-pdf -o test.pdf -i examples/cli/fill_pdf.csv 05xXsZko33JIO6aq5Pnr
736
+ ```
737
+
738
+ ## API Documentation
739
+
740
+ Our general API Documentation can be found [here](https://www.useanvil.com/api/). It's the best resource for up-to-date information about our API and its capabilities.
741
+
742
+ See the [PDF filling API docs](https://useanvil.com/docs/api/fill-pdf) for more information about the `fill_pdf` method.
743
+
744
+ ## Examples
745
+
746
+ The [examples](https://github.com/anvilco/python-anvil/tree/master/examples) folder has complete, runnable examples: filling a PDF, generating a PDF from Markdown or HTML, e-signing with a template or an uploaded PDF, embedded signing, workflow submissions, and your own GraphQL queries. Each takes your API key from `ANVIL_API_KEY`.
747
+
748
+ ## Development
749
+
750
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to run the tests, test against a real environment, regenerate the types, and publish a release.
751
+