python-anvil 2.0.0__tar.gz → 3.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_anvil-2.0.0 → python_anvil-3.0.1}/PKG-INFO +4 -6
- {python_anvil-2.0.0 → python_anvil-3.0.1}/README.md +1 -1
- {python_anvil-2.0.0 → python_anvil-3.0.1}/pyproject.toml +16 -12
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api.py +107 -49
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/create_etch_packet.py +23 -45
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/forge_submit.py +0 -2
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/payload.py +10 -16
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/requests.py +7 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/cli.py +2 -1
- python_anvil-3.0.1/python_anvil/constants.py +18 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/http.py +69 -13
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_api.py +73 -45
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_cli.py +41 -2
- python_anvil-2.0.0/python_anvil/multipart_helpers.py +0 -124
- python_anvil-2.0.0/python_anvil/tests/test_multipart_helpers.py +0 -38
- python_anvil-2.0.0/setup.py +0 -41
- {python_anvil-2.0.0 → python_anvil-3.0.1}/LICENSE.md +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/__init__.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/__init__.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/base.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/__init__.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/base.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/generate_etch_signing_url.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/helpers.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/exceptions.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/py.typed +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/__init__.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/conftest.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/payloads.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_http.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_utils.py +0 -0
- {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-anvil
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Anvil API
|
|
5
5
|
Home-page: https://www.useanvil.com/
|
|
6
6
|
License: MIT
|
|
@@ -18,17 +18,15 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
21
|
Classifier: Programming Language :: Python :: 3.7
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
26
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
23
|
Requires-Dist: click (>=8.0,<9.0)
|
|
24
|
+
Requires-Dist: gql[requests] (==3.5.0b1)
|
|
28
25
|
Requires-Dist: pydantic (>=1.8.2,<2.0.0)
|
|
29
26
|
Requires-Dist: ratelimit (>=2.2.1,<3.0.0)
|
|
30
27
|
Requires-Dist: requests (>=2.25.0,<3.0.0)
|
|
31
28
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
29
|
+
Requires-Dist: urllib3 (<2)
|
|
32
30
|
Project-URL: Documentation, https://github.com/anvilco/python-anvil
|
|
33
31
|
Project-URL: Repository, https://github.com/anvilco/python-anvil
|
|
34
32
|
Description-Content-Type: text/markdown
|
|
@@ -61,7 +59,7 @@ General API documentation: [Anvil API docs](https://www.useanvil.com/docs)
|
|
|
61
59
|
|
|
62
60
|
## Requirements
|
|
63
61
|
|
|
64
|
-
* Python 3.
|
|
62
|
+
* Python 3.7.2+
|
|
65
63
|
|
|
66
64
|
## Installation
|
|
67
65
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
|
|
3
3
|
name = "python_anvil"
|
|
4
|
-
version = "
|
|
4
|
+
version = "3.0.1"
|
|
5
5
|
description = "Anvil API"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
authors = ["Anvil Foundry Inc. <developers@useanvil.com>"]
|
|
@@ -34,13 +34,19 @@ classifiers = [
|
|
|
34
34
|
|
|
35
35
|
python = ">=3.7.2,<3.12"
|
|
36
36
|
|
|
37
|
+
# urllib must be pinned to <2 due to an issue with the `requests_toolbelt`
|
|
38
|
+
# dependency in `gql`.
|
|
39
|
+
# Similar: https://github.com/apache/arrow/commit/4295e3dd06f9f507f75ccc780eaca16a6b621a02
|
|
40
|
+
urllib3 = "<2"
|
|
41
|
+
|
|
37
42
|
click = "^8.0"
|
|
38
43
|
requests = "^2.25.0"
|
|
39
44
|
ratelimit = "^2.2.1"
|
|
40
45
|
tabulate = "^0.9.0"
|
|
41
46
|
pydantic = "^1.8.2"
|
|
47
|
+
gql = {version = "3.5.0b1", extras = ["requests"]}
|
|
42
48
|
|
|
43
|
-
[tool.poetry.dev
|
|
49
|
+
[tool.poetry.group.dev.dependencies]
|
|
44
50
|
|
|
45
51
|
# Formatters
|
|
46
52
|
black = "=22.12.0"
|
|
@@ -48,30 +54,28 @@ isort = "^5.11.4"
|
|
|
48
54
|
|
|
49
55
|
# Linters
|
|
50
56
|
mypy = "*"
|
|
51
|
-
pydocstyle = "6.
|
|
52
|
-
pylint = "2.
|
|
57
|
+
pydocstyle = "^6.3.0"
|
|
58
|
+
pylint = "^2.16.2"
|
|
53
59
|
|
|
54
60
|
# Testing
|
|
55
|
-
pytest = "^7.2.
|
|
56
|
-
pytest-cov = "
|
|
61
|
+
pytest = "^7.2.1"
|
|
62
|
+
pytest-cov = "^4.0.0"
|
|
57
63
|
pytest-describe = "^2.0"
|
|
58
|
-
pytest-random = "
|
|
64
|
+
pytest-random = "^0.2"
|
|
59
65
|
freezegun = "*"
|
|
60
66
|
|
|
61
67
|
# Reports
|
|
62
68
|
coveragespace = "*"
|
|
63
69
|
|
|
64
70
|
# Documentation
|
|
65
|
-
mkdocs = "1.4.2"
|
|
66
|
-
pygments = "^2.
|
|
71
|
+
mkdocs = "^1.4.2"
|
|
72
|
+
pygments = "^2.14.0"
|
|
67
73
|
|
|
68
74
|
# Tooling
|
|
69
|
-
pyinstaller = "^5.
|
|
75
|
+
pyinstaller = "^5.8.0"
|
|
70
76
|
sniffer = "*"
|
|
71
77
|
MacFSEvents = { version = "*", platform = "darwin" }
|
|
72
78
|
pync = { version = "*", platform = "darwin" }
|
|
73
|
-
# Updating `ipython` to 8.0+ will require a minimum Python of >=3.8
|
|
74
|
-
ipython = "^7.12.0"
|
|
75
79
|
pyinotify = {version = "^0.9.6", optional = true}
|
|
76
80
|
tox = "^3.21.2"
|
|
77
81
|
pre-commit = "^2.21.0"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
|
+
from gql import gql
|
|
3
|
+
from graphql import DocumentNode
|
|
2
4
|
from typing import Any, AnyStr, Callable, Dict, List, Optional, Text, Tuple, Union
|
|
3
5
|
|
|
4
6
|
from .api_resources.mutations import (
|
|
@@ -13,12 +15,11 @@ from .api_resources.payload import (
|
|
|
13
15
|
ForgeSubmitPayload,
|
|
14
16
|
GeneratePDFPayload,
|
|
15
17
|
)
|
|
16
|
-
from .api_resources.requests import
|
|
17
|
-
from .http import HTTPClient
|
|
18
|
-
from .multipart_helpers import get_multipart_payload
|
|
18
|
+
from .api_resources.requests import PlainRequest, RestRequest
|
|
19
|
+
from .http import GQLClient, HTTPClient
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
logger = getLogger(__name__)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def _get_return(res: Dict, get_data: Callable[[Dict], Union[Dict, List]]):
|
|
@@ -47,39 +48,70 @@ class Anvil:
|
|
|
47
48
|
# This is the default when a version is not provided.
|
|
48
49
|
VERSION_LATEST_PUBLISHED = -2
|
|
49
50
|
|
|
50
|
-
def __init__(
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
api_key: Optional[str] = None,
|
|
54
|
+
environment="dev",
|
|
55
|
+
endpoint_url=None,
|
|
56
|
+
):
|
|
57
|
+
if not api_key:
|
|
58
|
+
raise ValueError('`api_key` must be a valid string')
|
|
59
|
+
|
|
51
60
|
self.client = HTTPClient(api_key=api_key, environment=environment)
|
|
61
|
+
self.gql_client = GQLClient.get_client(
|
|
62
|
+
api_key=api_key,
|
|
63
|
+
environment=environment,
|
|
64
|
+
endpoint_url=endpoint_url,
|
|
65
|
+
)
|
|
52
66
|
|
|
53
67
|
def query(
|
|
54
68
|
self,
|
|
55
|
-
query: str,
|
|
56
|
-
variables:
|
|
69
|
+
query: Union[str, DocumentNode],
|
|
70
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
57
71
|
**kwargs,
|
|
58
72
|
):
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
"""Execute a GraphQL query.
|
|
74
|
+
|
|
75
|
+
:param query:
|
|
76
|
+
:type query: Union[str, DocumentNode]
|
|
77
|
+
:param variables:
|
|
78
|
+
:type variables: Optional[Dict[str, Any]]
|
|
79
|
+
:param kwargs:
|
|
80
|
+
:return:
|
|
81
|
+
"""
|
|
82
|
+
# Remove `debug` for now.
|
|
83
|
+
kwargs.pop("debug", None)
|
|
84
|
+
if isinstance(query, str):
|
|
85
|
+
query = gql(query)
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
gql = GraphqlRequest(client=self.client)
|
|
64
|
-
return gql.post(query.get_mutation(), variables, **kwargs)
|
|
87
|
+
return self.gql_client.execute(query, variable_values=variables, **kwargs)
|
|
65
88
|
|
|
66
|
-
def
|
|
89
|
+
def mutate(
|
|
90
|
+
self, query: Union[str, BaseQuery], variables: Dict[str, Any], **kwargs
|
|
91
|
+
) -> Dict[str, Any]:
|
|
67
92
|
"""
|
|
68
|
-
|
|
93
|
+
Execute a GraphQL mutation.
|
|
69
94
|
|
|
70
|
-
|
|
95
|
+
NOTE: Any files attached provided in `variables` will be sent via the
|
|
96
|
+
multipart spec:
|
|
71
97
|
https://github.com/jaydenseric/graphql-multipart-request-spec
|
|
72
98
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:param files:
|
|
99
|
+
:param query:
|
|
100
|
+
:type query: Union[str, BaseQuery]
|
|
101
|
+
:param variables:
|
|
102
|
+
:type variables: Dict[str, Any]
|
|
78
103
|
:param kwargs:
|
|
79
104
|
:return:
|
|
80
105
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
# Remove `debug` for now.
|
|
107
|
+
kwargs.pop("debug", None)
|
|
108
|
+
if isinstance(query, str):
|
|
109
|
+
use_query = gql(query)
|
|
110
|
+
else:
|
|
111
|
+
mutation = query.get_mutation()
|
|
112
|
+
use_query = gql(mutation)
|
|
113
|
+
|
|
114
|
+
return self.gql_client.execute(use_query, variable_values=variables, **kwargs)
|
|
83
115
|
|
|
84
116
|
def request_rest(self, options: Optional[dict] = None):
|
|
85
117
|
api = RestRequest(self.client, options=options)
|
|
@@ -180,20 +212,34 @@ class Anvil:
|
|
|
180
212
|
arg_str = f"({','.join(joined_args)})"
|
|
181
213
|
|
|
182
214
|
res = self.query(
|
|
183
|
-
|
|
215
|
+
gql(
|
|
216
|
+
f"""{{
|
|
184
217
|
cast {arg_str} {{
|
|
185
218
|
{" ".join(fields)}
|
|
186
219
|
}}
|
|
187
|
-
}}"""
|
|
220
|
+
}}"""
|
|
221
|
+
),
|
|
188
222
|
**kwargs,
|
|
189
223
|
)
|
|
190
224
|
|
|
191
|
-
def get_data(r) -> Dict[str, Any]:
|
|
192
|
-
return r["
|
|
225
|
+
def get_data(r: dict) -> Dict[str, Any]:
|
|
226
|
+
return r["cast"]
|
|
193
227
|
|
|
194
228
|
return _get_return(res, get_data=get_data)
|
|
195
229
|
|
|
196
|
-
def get_casts(
|
|
230
|
+
def get_casts(
|
|
231
|
+
self, fields: Optional[List[str]] = None, show_all: bool = False, **kwargs
|
|
232
|
+
) -> List[Dict[str, Any]]:
|
|
233
|
+
"""Retrieve all Cast objects for the current user across all organizations.
|
|
234
|
+
|
|
235
|
+
:param fields: List of fields to retrieve for each cast object
|
|
236
|
+
:type fields: Optional[List[str]]
|
|
237
|
+
:param show_all: Boolean to show all Cast objects.
|
|
238
|
+
Defaults to showing only templates.
|
|
239
|
+
:type show_all: bool
|
|
240
|
+
:param kwargs:
|
|
241
|
+
:return:
|
|
242
|
+
"""
|
|
197
243
|
if not fields:
|
|
198
244
|
# Use default fields
|
|
199
245
|
fields = ["eid", "title", "fieldInfo"]
|
|
@@ -201,7 +247,8 @@ class Anvil:
|
|
|
201
247
|
cast_args = "" if show_all else "(isTemplate: true)"
|
|
202
248
|
|
|
203
249
|
res = self.query(
|
|
204
|
-
|
|
250
|
+
gql(
|
|
251
|
+
f"""{{
|
|
205
252
|
currentUser {{
|
|
206
253
|
organizations {{
|
|
207
254
|
casts {cast_args} {{
|
|
@@ -209,19 +256,26 @@ class Anvil:
|
|
|
209
256
|
}}
|
|
210
257
|
}}
|
|
211
258
|
}}
|
|
212
|
-
}}"""
|
|
259
|
+
}}"""
|
|
260
|
+
),
|
|
213
261
|
**kwargs,
|
|
214
262
|
)
|
|
215
263
|
|
|
216
|
-
def get_data(r):
|
|
217
|
-
orgs = r["
|
|
264
|
+
def get_data(r: dict):
|
|
265
|
+
orgs = r["currentUser"]["organizations"]
|
|
218
266
|
return [item for org in orgs for item in org["casts"]]
|
|
219
267
|
|
|
220
268
|
return _get_return(res, get_data=get_data)
|
|
221
269
|
|
|
222
270
|
def get_current_user(self, **kwargs):
|
|
271
|
+
"""Retrieve current user data.
|
|
272
|
+
|
|
273
|
+
:param kwargs:
|
|
274
|
+
:return:
|
|
275
|
+
"""
|
|
223
276
|
res = self.query(
|
|
224
|
-
|
|
277
|
+
gql(
|
|
278
|
+
"""{
|
|
225
279
|
currentUser {
|
|
226
280
|
name
|
|
227
281
|
email
|
|
@@ -237,22 +291,23 @@ class Anvil:
|
|
|
237
291
|
}
|
|
238
292
|
}
|
|
239
293
|
}
|
|
240
|
-
}
|
|
241
|
-
|
|
294
|
+
}"""
|
|
295
|
+
),
|
|
242
296
|
**kwargs,
|
|
243
297
|
)
|
|
244
298
|
|
|
245
|
-
return _get_return(res, get_data=lambda r: r["
|
|
299
|
+
return _get_return(res, get_data=lambda r: r["currentUser"])
|
|
246
300
|
|
|
247
301
|
def get_welds(self, **kwargs) -> Union[List, Tuple[List, Dict]]:
|
|
248
302
|
res = self.query(
|
|
249
|
-
|
|
303
|
+
gql(
|
|
304
|
+
"""{
|
|
250
305
|
currentUser {
|
|
251
306
|
organizations {
|
|
252
307
|
welds {
|
|
253
308
|
eid
|
|
254
309
|
slug
|
|
255
|
-
|
|
310
|
+
name
|
|
256
311
|
forges {
|
|
257
312
|
eid
|
|
258
313
|
name
|
|
@@ -260,19 +315,21 @@ class Anvil:
|
|
|
260
315
|
}
|
|
261
316
|
}
|
|
262
317
|
}
|
|
263
|
-
}"""
|
|
318
|
+
}"""
|
|
319
|
+
),
|
|
264
320
|
**kwargs,
|
|
265
321
|
)
|
|
266
322
|
|
|
267
|
-
def get_data(r):
|
|
268
|
-
orgs = r["
|
|
323
|
+
def get_data(r: dict):
|
|
324
|
+
orgs = r["currentUser"]["organizations"]
|
|
269
325
|
return [item for org in orgs for item in org["welds"]]
|
|
270
326
|
|
|
271
327
|
return _get_return(res, get_data=get_data)
|
|
272
328
|
|
|
273
329
|
def get_weld(self, eid: Text, **kwargs):
|
|
274
330
|
res = self.query(
|
|
275
|
-
|
|
331
|
+
gql(
|
|
332
|
+
"""
|
|
276
333
|
query WeldQuery(
|
|
277
334
|
#$organizationSlug: String!,
|
|
278
335
|
#$slug: String!
|
|
@@ -292,13 +349,14 @@ class Anvil:
|
|
|
292
349
|
slug
|
|
293
350
|
}
|
|
294
351
|
}
|
|
295
|
-
}"""
|
|
352
|
+
}"""
|
|
353
|
+
),
|
|
296
354
|
variables=dict(eid=eid),
|
|
297
355
|
**kwargs,
|
|
298
356
|
)
|
|
299
357
|
|
|
300
|
-
def get_data(r):
|
|
301
|
-
return r["
|
|
358
|
+
def get_data(r: dict):
|
|
359
|
+
return r["weld"]
|
|
302
360
|
|
|
303
361
|
return _get_return(res, get_data=get_data)
|
|
304
362
|
|
|
@@ -337,10 +395,10 @@ class Anvil:
|
|
|
337
395
|
"`payload` must be a valid CreateEtchPacket instance or dict"
|
|
338
396
|
)
|
|
339
397
|
|
|
340
|
-
|
|
398
|
+
payload = mutation.create_payload()
|
|
399
|
+
variables = payload.dict(by_alias=True, exclude_none=True)
|
|
341
400
|
|
|
342
|
-
|
|
343
|
-
return res
|
|
401
|
+
return self.mutate(mutation, variables=variables, upload_files=True, **kwargs)
|
|
344
402
|
|
|
345
403
|
def generate_etch_signing_url(self, signer_eid: str, client_user_id: str, **kwargs):
|
|
346
404
|
"""Generate a signing URL for a given user."""
|
|
@@ -361,7 +419,7 @@ class Anvil:
|
|
|
361
419
|
payload: Optional[Union[Dict[Text, Any], ForgeSubmitPayload]] = None,
|
|
362
420
|
json=None,
|
|
363
421
|
**kwargs,
|
|
364
|
-
):
|
|
422
|
+
) -> Dict[str, Any]:
|
|
365
423
|
"""Create a Webform (forge) submission via a graphql mutation."""
|
|
366
424
|
if not any([json, payload]):
|
|
367
425
|
raise TypeError('One of arguments `json` or `payload` are required')
|
{python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/create_etch_packet.py
RENAMED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# pylint: disable=too-many-instance-attributes
|
|
2
|
-
|
|
3
|
-
from
|
|
2
|
+
import logging
|
|
3
|
+
from io import BufferedIOBase
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from mimetypes import guess_type
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
7
|
|
|
5
8
|
from python_anvil.api_resources.mutations.base import BaseQuery
|
|
6
9
|
from python_anvil.api_resources.payload import (
|
|
7
10
|
AttachableEtchFile,
|
|
8
|
-
Base64Upload,
|
|
9
11
|
CreateEtchFilePayload,
|
|
10
12
|
CreateEtchPacketPayload,
|
|
11
13
|
DocumentUpload,
|
|
@@ -14,6 +16,8 @@ from python_anvil.api_resources.payload import (
|
|
|
14
16
|
from python_anvil.utils import create_unique_id
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
logger: Logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
17
21
|
DEFAULT_RESPONSE_QUERY = """{
|
|
18
22
|
id
|
|
19
23
|
eid
|
|
@@ -193,6 +197,18 @@ class CreateEtchPacket(BaseQuery):
|
|
|
193
197
|
Files will not be uploaded when running this method. They will be
|
|
194
198
|
uploaded when the mutation actually runs.
|
|
195
199
|
"""
|
|
200
|
+
if (
|
|
201
|
+
isinstance(file, DocumentUpload)
|
|
202
|
+
and isinstance(file.file, BufferedIOBase)
|
|
203
|
+
and getattr(file.file, "content_type", None) is None
|
|
204
|
+
):
|
|
205
|
+
# Don't clobber existing `content_type`s provided.
|
|
206
|
+
content_type, _ = guess_type(file.file.name) # type: ignore
|
|
207
|
+
logger.debug(
|
|
208
|
+
"File did not have a `content_type`, guessing as '%s'", content_type
|
|
209
|
+
)
|
|
210
|
+
file.file.content_type = content_type # type: ignore
|
|
211
|
+
|
|
196
212
|
self.files.append(file)
|
|
197
213
|
|
|
198
214
|
def add_file_payloads(self, file_id: str, fill_payload):
|
|
@@ -215,43 +231,7 @@ class CreateEtchPacket(BaseQuery):
|
|
|
215
231
|
)
|
|
216
232
|
return self.file_payloads
|
|
217
233
|
|
|
218
|
-
def
|
|
219
|
-
"""Scan through and gather files for use in multipart requests.
|
|
220
|
-
|
|
221
|
-
During the process, files will be replaced with a dummy `Path`
|
|
222
|
-
instance as `Callable` instances and likely their returns of some
|
|
223
|
-
sort of IO reader is not serializable and not easily dealt with in
|
|
224
|
-
pydantic (the underlying model system for the types used in this
|
|
225
|
-
library). Closer to the actual request, when the multipart payload is
|
|
226
|
-
being constructed, the dummy instances will be replaced by their actual
|
|
227
|
-
file instances.
|
|
228
|
-
|
|
229
|
-
:return: Tuple containing a list of uploadable file types, and a list
|
|
230
|
-
of actual file references
|
|
231
|
-
:rtype: Tuple[List, List]
|
|
232
|
-
"""
|
|
233
|
-
contains_uploads = any( # pylint: disable=use-a-generator
|
|
234
|
-
[isinstance(f, DocumentUpload) for f in self.files]
|
|
235
|
-
)
|
|
236
|
-
if not contains_uploads:
|
|
237
|
-
return self.files, []
|
|
238
|
-
|
|
239
|
-
files = []
|
|
240
|
-
for f in self.files:
|
|
241
|
-
if not isinstance(f, DocumentUpload):
|
|
242
|
-
continue
|
|
243
|
-
|
|
244
|
-
if getattr(f.Config, "contains_uploads", False):
|
|
245
|
-
attr_name = getattr(f.Config, "contains_uploads")
|
|
246
|
-
upload = getattr(f, attr_name)
|
|
247
|
-
|
|
248
|
-
if not isinstance(upload, Base64Upload):
|
|
249
|
-
files.append(upload)
|
|
250
|
-
setattr(f, attr_name, Path())
|
|
251
|
-
|
|
252
|
-
return self.files, files
|
|
253
|
-
|
|
254
|
-
def create_payload(self) -> Tuple[CreateEtchPacketPayload, List]:
|
|
234
|
+
def create_payload(self) -> CreateEtchPacketPayload:
|
|
255
235
|
"""Create a payload based on data set on the class instance.
|
|
256
236
|
|
|
257
237
|
Check `api_resources.payload.CreateEtchPacketPayload` for full payload
|
|
@@ -260,19 +240,17 @@ class CreateEtchPacket(BaseQuery):
|
|
|
260
240
|
"""
|
|
261
241
|
# If there's an existing payload instance attribute, just return that.
|
|
262
242
|
if self.payload:
|
|
263
|
-
return self.payload
|
|
243
|
+
return self.payload
|
|
264
244
|
|
|
265
245
|
if not self.name:
|
|
266
246
|
raise TypeError("`name` and `signature_email_subject` cannot be None")
|
|
267
247
|
|
|
268
|
-
payload_files, file_refs = self.get_files_for_payload()
|
|
269
|
-
|
|
270
248
|
payload = CreateEtchPacketPayload(
|
|
271
249
|
is_test=self.is_test,
|
|
272
250
|
is_draft=self.is_draft,
|
|
273
251
|
name=self.name,
|
|
274
252
|
signers=self.signers,
|
|
275
|
-
files=
|
|
253
|
+
files=self.files,
|
|
276
254
|
data=CreateEtchFilePayload(payloads=self.get_file_payloads()),
|
|
277
255
|
signature_email_subject=self.signature_email_subject,
|
|
278
256
|
signature_email_body=self.signature_email_body,
|
|
@@ -286,4 +264,4 @@ class CreateEtchPacket(BaseQuery):
|
|
|
286
264
|
duplicate_casts=self.duplicate_casts,
|
|
287
265
|
)
|
|
288
266
|
|
|
289
|
-
return payload
|
|
267
|
+
return payload
|
{python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/forge_submit.py
RENAMED
|
@@ -47,7 +47,6 @@ mutation ForgeSubmit(
|
|
|
47
47
|
$timezone: String,
|
|
48
48
|
$groupArrayId: String,
|
|
49
49
|
$groupArrayIndex: Int,
|
|
50
|
-
$errorType: String,
|
|
51
50
|
$webhookURL: String,
|
|
52
51
|
) {{
|
|
53
52
|
forgeSubmit (
|
|
@@ -61,7 +60,6 @@ mutation ForgeSubmit(
|
|
|
61
60
|
timezone: $timezone,
|
|
62
61
|
groupArrayId: $groupArrayId,
|
|
63
62
|
groupArrayIndex: $groupArrayIndex,
|
|
64
|
-
errorType: $errorType,
|
|
65
63
|
webhookURL: $webhookURL
|
|
66
64
|
) {query}
|
|
67
65
|
}}
|
|
@@ -7,12 +7,11 @@ from io import BufferedIOBase
|
|
|
7
7
|
# import `BaseModel` and it's not broken, so let's keep it.
|
|
8
8
|
from pydantic import ( # pylint: disable=no-name-in-module
|
|
9
9
|
Field,
|
|
10
|
-
FilePath,
|
|
11
10
|
HttpUrl,
|
|
12
11
|
root_validator,
|
|
13
12
|
validator,
|
|
14
13
|
)
|
|
15
|
-
from typing import Any,
|
|
14
|
+
from typing import Any, Dict, List, Optional, Text, Union
|
|
16
15
|
|
|
17
16
|
from .base import BaseModel
|
|
18
17
|
|
|
@@ -184,7 +183,13 @@ class DocumentUpload(BaseModel):
|
|
|
184
183
|
|
|
185
184
|
id: str
|
|
186
185
|
title: str
|
|
187
|
-
|
|
186
|
+
# Previously "UploadableFile", however, that seems to cause weird upload
|
|
187
|
+
# issues where a PDF file would have its first few bytes removed.
|
|
188
|
+
# We're now relying on the backend to validate this property instead of on
|
|
189
|
+
# the client library side.
|
|
190
|
+
# This might be a bug on the `pydantic` side(?) when this object gets
|
|
191
|
+
# converted into a dict.
|
|
192
|
+
file: Any
|
|
188
193
|
fields: List[SignatureField]
|
|
189
194
|
font_size: int = 14
|
|
190
195
|
text_color: str = "#000000"
|
|
@@ -192,10 +197,8 @@ class DocumentUpload(BaseModel):
|
|
|
192
197
|
class Config:
|
|
193
198
|
"""Special rules for this model class."""
|
|
194
199
|
|
|
200
|
+
# Needed to allow `BufferedIOBase` as an allowed `file` type.
|
|
195
201
|
arbitrary_types_allowed = True
|
|
196
|
-
# String to help point out the file upload attr.
|
|
197
|
-
# Used in multipart uploads and should not affect base64-encoded files.
|
|
198
|
-
contains_uploads = "file"
|
|
199
202
|
|
|
200
203
|
|
|
201
204
|
class EtchCastRef(BaseModel):
|
|
@@ -271,16 +274,7 @@ class ForgeSubmitPayload(BaseModel):
|
|
|
271
274
|
return values
|
|
272
275
|
|
|
273
276
|
|
|
274
|
-
|
|
275
|
-
# Base64Upload: An entire PDF file base64 encoded
|
|
276
|
-
# FilePath: The absolute path of the file. When the pydantic model is used,
|
|
277
|
-
# it will convert a path string (i.e. "/home/me/my_file.pdf") into a
|
|
278
|
-
# `pathlib.Path` instance.
|
|
279
|
-
# UploadMethod: A function that returns a tuple of:
|
|
280
|
-
# * BufferedIOBase instance for use in `requests`
|
|
281
|
-
# * String containing the name for the file for use in Anvil
|
|
282
|
-
UploadMethod = Callable[[Any], Tuple[BufferedIOBase, str]]
|
|
283
|
-
UploadableFile = Union[Base64Upload, FilePath, UploadMethod]
|
|
277
|
+
UploadableFile = Union[Base64Upload, BufferedIOBase]
|
|
284
278
|
AttachableEtchFile = Union[
|
|
285
279
|
DocumentUpload, EtchCastRef, DocumentMarkup, DocumentMarkdown
|
|
286
280
|
]
|
|
@@ -34,6 +34,7 @@ class AnvilRequest:
|
|
|
34
34
|
else:
|
|
35
35
|
message = f"Error: {status_code}: {response} {extra}"
|
|
36
36
|
|
|
37
|
+
# pylint: disable=broad-exception-raised
|
|
37
38
|
raise Exception(message)
|
|
38
39
|
|
|
39
40
|
def process_response(self, response, status_code, headers, **kwargs):
|
|
@@ -78,6 +79,12 @@ class BaseAnvilHttpRequest(AnvilRequest):
|
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
class GraphqlRequest(AnvilRequest):
|
|
82
|
+
"""Create a GraphQL request.
|
|
83
|
+
|
|
84
|
+
.. deprecated :: 2.0.0
|
|
85
|
+
Use `python_anvil.http.GQLClient` to make GraphQL queries and mutations.
|
|
86
|
+
"""
|
|
87
|
+
|
|
81
88
|
API_HOST = "https://graphql.useanvil.com"
|
|
82
89
|
|
|
83
90
|
def __init__(self, client: HTTPClient):
|
|
@@ -102,11 +102,12 @@ def weld(ctx, eid, list_all):
|
|
|
102
102
|
if debug:
|
|
103
103
|
click.echo(headers)
|
|
104
104
|
|
|
105
|
-
data = [(w["eid"], w.get("slug"), w.get("
|
|
105
|
+
data = [(w["eid"], w.get("slug"), w.get("name"), w.get("forges")) for w in res]
|
|
106
106
|
click.echo(tabulate(data, tablefmt="pretty", headers=["eid", "slug", "title"]))
|
|
107
107
|
return
|
|
108
108
|
|
|
109
109
|
if not eid:
|
|
110
|
+
# pylint: disable=broad-exception-raised
|
|
110
111
|
raise Exception("You need to pass in a weld eid")
|
|
111
112
|
|
|
112
113
|
res = anvil.get_weld(eid)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Basic constants used in the library."""
|
|
2
|
+
|
|
3
|
+
GRAPHQL_ENDPOINT: str = "https://graphql.useanvil.com"
|
|
4
|
+
REST_ENDPOINT = "https://app.useanvil.com/api/v1/"
|
|
5
|
+
|
|
6
|
+
RETRIES_LIMIT = 5
|
|
7
|
+
REQUESTS_LIMIT = {
|
|
8
|
+
"dev": {
|
|
9
|
+
"calls": 2,
|
|
10
|
+
"seconds": 1,
|
|
11
|
+
},
|
|
12
|
+
"prod": {
|
|
13
|
+
"calls": 40,
|
|
14
|
+
"seconds": 1,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
RATELIMIT_ENV = "dev"
|