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.
Files changed (32) hide show
  1. {python_anvil-2.0.0 → python_anvil-3.0.1}/PKG-INFO +4 -6
  2. {python_anvil-2.0.0 → python_anvil-3.0.1}/README.md +1 -1
  3. {python_anvil-2.0.0 → python_anvil-3.0.1}/pyproject.toml +16 -12
  4. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api.py +107 -49
  5. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/create_etch_packet.py +23 -45
  6. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/forge_submit.py +0 -2
  7. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/payload.py +10 -16
  8. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/requests.py +7 -0
  9. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/cli.py +2 -1
  10. python_anvil-3.0.1/python_anvil/constants.py +18 -0
  11. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/http.py +69 -13
  12. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_api.py +73 -45
  13. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_cli.py +41 -2
  14. python_anvil-2.0.0/python_anvil/multipart_helpers.py +0 -124
  15. python_anvil-2.0.0/python_anvil/tests/test_multipart_helpers.py +0 -38
  16. python_anvil-2.0.0/setup.py +0 -41
  17. {python_anvil-2.0.0 → python_anvil-3.0.1}/LICENSE.md +0 -0
  18. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/__init__.py +0 -0
  19. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/__init__.py +0 -0
  20. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/base.py +0 -0
  21. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/__init__.py +0 -0
  22. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/base.py +0 -0
  23. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/generate_etch_signing_url.py +0 -0
  24. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/api_resources/mutations/helpers.py +0 -0
  25. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/exceptions.py +0 -0
  26. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/py.typed +0 -0
  27. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/__init__.py +0 -0
  28. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/conftest.py +0 -0
  29. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/payloads.py +0 -0
  30. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_http.py +0 -0
  31. {python_anvil-2.0.0 → python_anvil-3.0.1}/python_anvil/tests/test_utils.py +0 -0
  32. {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: 2.0.0
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.6.2+
62
+ * Python 3.7.2+
65
63
 
66
64
  ## Installation
67
65
 
@@ -26,7 +26,7 @@ General API documentation: [Anvil API docs](https://www.useanvil.com/docs)
26
26
 
27
27
  ## Requirements
28
28
 
29
- * Python 3.6.2+
29
+ * Python 3.7.2+
30
30
 
31
31
  ## Installation
32
32
 
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
 
3
3
  name = "python_anvil"
4
- version = "2.0.0"
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-dependencies]
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.2.3"
52
- pylint = "2.15.10"
57
+ pydocstyle = "^6.3.0"
58
+ pylint = "^2.16.2"
53
59
 
54
60
  # Testing
55
- pytest = "^7.2.0"
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.5.2"
71
+ mkdocs = "^1.4.2"
72
+ pygments = "^2.14.0"
67
73
 
68
74
  # Tooling
69
- pyinstaller = "^5.7.0"
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
- from logging import getLogger
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 GraphqlRequest, PlainRequest, RestRequest
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__(self, api_key=None, environment='dev'):
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: Union[Optional[Text], Dict[Text, Any]] = None,
69
+ query: Union[str, DocumentNode],
70
+ variables: Optional[Dict[str, Any]] = None,
57
71
  **kwargs,
58
72
  ):
59
- gql = GraphqlRequest(client=self.client)
60
- return gql.post(query, variables=variables, **kwargs)
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
- def mutate(self, query: BaseQuery, variables: dict, **kwargs):
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 mutate_multipart(self, files: Dict, **kwargs):
89
+ def mutate(
90
+ self, query: Union[str, BaseQuery], variables: Dict[str, Any], **kwargs
91
+ ) -> Dict[str, Any]:
67
92
  """
68
- Multipart version of `mutate`.
93
+ Execute a GraphQL mutation.
69
94
 
70
- This will send a mutation based on the multipart spec defined here:
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
- Note that `variables` and `query` have been removed and replaced with
74
- `files`. The entire GraphQL payload should already be prepared as a
75
- `dict` beforehand.
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
- gql = GraphqlRequest(client=self.client)
82
- return gql.post_multipart(files=files, **kwargs)
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
- f"""{{
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["data"]["cast"]
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(self, fields=None, show_all=False, **kwargs) -> List[Dict[str, Any]]:
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
- f"""{{
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["data"]["currentUser"]["organizations"]
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["data"]["currentUser"])
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
- title
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["data"]["currentUser"]["organizations"]
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["data"]["weld"]
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
- files = get_multipart_payload(mutation)
398
+ payload = mutation.create_payload()
399
+ variables = payload.dict(by_alias=True, exclude_none=True)
341
400
 
342
- res = self.mutate_multipart(files, **kwargs)
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')
@@ -1,11 +1,13 @@
1
1
  # pylint: disable=too-many-instance-attributes
2
- from pathlib import Path
3
- from typing import Any, Dict, List, Optional, Tuple, Union
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 get_files_for_payload(self) -> Tuple[List[AttachableEtchFile], List[Any]]:
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=payload_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, file_refs
267
+ return payload
@@ -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, Callable, Dict, List, Optional, Text, Tuple, Union
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
- file: "UploadableFile"
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
- # Types usable for uploads:
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("title"), w.get("forges")) for w in res]
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"