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.
- python_anvil-6.0.0/PKG-INFO +751 -0
- python_anvil-6.0.0/README.md +716 -0
- python_anvil-6.0.0/pyproject.toml +149 -0
- python_anvil-6.0.0/python_anvil/__init__.py +26 -0
- python_anvil-6.0.0/python_anvil/api.py +698 -0
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/base.py +1 -4
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/__init__.py +8 -0
- python_anvil-6.0.0/python_anvil/api_resources/mutations/base.py +33 -0
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/create_etch_packet.py +63 -108
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/forge_submit.py +22 -69
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/mutations/generate_etch_signing_url.py +8 -8
- python_anvil-6.0.0/python_anvil/api_resources/mutations/helpers.py +5 -0
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/payload.py +86 -103
- python_anvil-6.0.0/python_anvil/auth.py +60 -0
- python_anvil-6.0.0/python_anvil/cli.py +326 -0
- python_anvil-6.0.0/python_anvil/constants.py +43 -0
- python_anvil-6.0.0/python_anvil/errors.py +132 -0
- python_anvil-6.0.0/python_anvil/exceptions.py +9 -0
- python_anvil-6.0.0/python_anvil/graphql/__init__.py +11 -0
- python_anvil-6.0.0/python_anvil/graphql/mutations/__init__.py +14 -0
- python_anvil-6.0.0/python_anvil/graphql/mutations/create_etch_packet.py +79 -0
- python_anvil-6.0.0/python_anvil/graphql/mutations/forge_submit.py +60 -0
- python_anvil-6.0.0/python_anvil/graphql/mutations/generate_etch_sign_url.py +12 -0
- python_anvil-6.0.0/python_anvil/graphql/mutations/remove_weld_data.py +9 -0
- python_anvil-6.0.0/python_anvil/graphql/queries/__init__.py +4 -0
- python_anvil-6.0.0/python_anvil/graphql/queries/etch_packet.py +45 -0
- python_anvil-6.0.0/python_anvil/graphql/selection.py +15 -0
- python_anvil-6.0.0/python_anvil/http.py +316 -0
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/models.py +40 -18
- python_anvil-6.0.0/python_anvil/rate_limit.py +272 -0
- python_anvil-6.0.0/python_anvil/types/__init__.py +58 -0
- python_anvil-6.0.0/python_anvil/types/_base.py +21 -0
- python_anvil-6.0.0/python_anvil/types/graphql.py +2626 -0
- python_anvil-6.0.0/python_anvil/types/operations.py +1261 -0
- python_anvil-6.0.0/python_anvil/types/rest.py +700 -0
- python_anvil-6.0.0/python_anvil/upload.py +269 -0
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/utils.py +6 -1
- python_anvil-5.1.2/PKG-INFO +0 -77
- python_anvil-5.1.2/README.md +0 -43
- python_anvil-5.1.2/pyproject.toml +0 -103
- python_anvil-5.1.2/python_anvil/__init__.py +0 -12
- python_anvil-5.1.2/python_anvil/api.py +0 -447
- python_anvil-5.1.2/python_anvil/api_resources/mutations/base.py +0 -20
- python_anvil-5.1.2/python_anvil/api_resources/mutations/helpers.py +0 -7
- python_anvil-5.1.2/python_anvil/api_resources/requests.py +0 -183
- python_anvil-5.1.2/python_anvil/cli.py +0 -363
- python_anvil-5.1.2/python_anvil/constants.py +0 -25
- python_anvil-5.1.2/python_anvil/exceptions.py +0 -6
- python_anvil-5.1.2/python_anvil/http.py +0 -226
- python_anvil-5.1.2/python_anvil/tests/__init__.py +0 -1
- python_anvil-5.1.2/python_anvil/tests/conftest.py +0 -11
- python_anvil-5.1.2/python_anvil/tests/payloads.py +0 -92
- python_anvil-5.1.2/python_anvil/tests/test_api.py +0 -457
- python_anvil-5.1.2/python_anvil/tests/test_cli.py +0 -112
- python_anvil-5.1.2/python_anvil/tests/test_http.py +0 -145
- python_anvil-5.1.2/python_anvil/tests/test_models.py +0 -264
- python_anvil-5.1.2/python_anvil/tests/test_utils.py +0 -39
- {python_anvil-5.1.2 → python_anvil-6.0.0}/LICENSE.md +0 -0
- {python_anvil-5.1.2 → python_anvil-6.0.0}/python_anvil/api_resources/__init__.py +0 -0
- {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
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
# Anvil API Client for Python
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/python-anvil)
|
|
41
|
+
[](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
|
+
|