python-anvil 3.0.1__tar.gz → 5.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-3.0.1 → python_anvil-5.0.0}/PKG-INFO +7 -9
- {python_anvil-3.0.1 → python_anvil-5.0.0}/README.md +1 -1
- {python_anvil-3.0.1 → python_anvil-5.0.0}/pyproject.toml +17 -14
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/__init__.py +3 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api.py +18 -20
- python_anvil-5.0.0/python_anvil/api_resources/base.py +31 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/create_etch_packet.py +0 -3
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/forge_submit.py +6 -8
- python_anvil-5.0.0/python_anvil/api_resources/mutations/helpers.py +7 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/payload.py +33 -52
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/cli.py +1 -1
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/http.py +13 -0
- python_anvil-5.0.0/python_anvil/models.py +75 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/test_api.py +2 -39
- python_anvil-5.0.0/python_anvil/tests/test_models.py +264 -0
- python_anvil-3.0.1/python_anvil/api_resources/base.py +0 -30
- python_anvil-3.0.1/python_anvil/api_resources/mutations/helpers.py +0 -7
- {python_anvil-3.0.1 → python_anvil-5.0.0}/LICENSE.md +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/__init__.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/__init__.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/base.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/generate_etch_signing_url.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/requests.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/constants.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/exceptions.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/py.typed +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/__init__.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/conftest.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/payloads.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/test_cli.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/test_http.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/tests/test_utils.py +0 -0
- {python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/utils.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version:
|
|
2
|
+
Name: python_anvil
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Anvil API
|
|
5
5
|
Home-page: https://www.useanvil.com/
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: anvil,api,pdf,signing
|
|
8
8
|
Author: Anvil Foundry Inc.
|
|
9
9
|
Author-email: developers@useanvil.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.8.0,<3.12
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Natural Language :: English
|
|
@@ -18,15 +18,13 @@ 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.7
|
|
22
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
22
|
Requires-Dist: click (>=8.0,<9.0)
|
|
24
|
-
Requires-Dist: gql[requests] (==3.
|
|
25
|
-
Requires-Dist: pydantic (>=
|
|
23
|
+
Requires-Dist: gql[requests] (==3.6.0b1)
|
|
24
|
+
Requires-Dist: pydantic (>=2.6.1,<3.0.0)
|
|
26
25
|
Requires-Dist: ratelimit (>=2.2.1,<3.0.0)
|
|
27
|
-
Requires-Dist: requests (>=2.
|
|
26
|
+
Requires-Dist: requests (>=2.28.2,<3.0.0)
|
|
28
27
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
29
|
-
Requires-Dist: urllib3 (<2)
|
|
30
28
|
Project-URL: Documentation, https://github.com/anvilco/python-anvil
|
|
31
29
|
Project-URL: Repository, https://github.com/anvilco/python-anvil
|
|
32
30
|
Description-Content-Type: text/markdown
|
|
@@ -59,7 +57,7 @@ General API documentation: [Anvil API docs](https://www.useanvil.com/docs)
|
|
|
59
57
|
|
|
60
58
|
## Requirements
|
|
61
59
|
|
|
62
|
-
* Python 3.
|
|
60
|
+
* Python >=3.8
|
|
63
61
|
|
|
64
62
|
## Installation
|
|
65
63
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
|
|
3
3
|
name = "python_anvil"
|
|
4
|
-
version = "
|
|
4
|
+
version = "5.0.0"
|
|
5
5
|
description = "Anvil API"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
authors = ["Anvil Foundry Inc. <developers@useanvil.com>"]
|
|
@@ -23,28 +23,23 @@ classifiers = [
|
|
|
23
23
|
"Operating System :: OS Independent",
|
|
24
24
|
"Programming Language :: Python",
|
|
25
25
|
"Programming Language :: Python :: 3",
|
|
26
|
-
"Programming Language :: Python :: 3.7",
|
|
27
26
|
"Programming Language :: Python :: 3.8",
|
|
28
27
|
"Programming Language :: Python :: 3.9",
|
|
29
28
|
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
30
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[tool.poetry.dependencies]
|
|
34
34
|
|
|
35
|
-
python = ">=3.
|
|
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"
|
|
35
|
+
python = ">=3.8.0,<3.12"
|
|
41
36
|
|
|
42
37
|
click = "^8.0"
|
|
43
|
-
requests = "^2.
|
|
38
|
+
requests = "^2.28.2"
|
|
44
39
|
ratelimit = "^2.2.1"
|
|
45
40
|
tabulate = "^0.9.0"
|
|
46
|
-
pydantic = "^
|
|
47
|
-
gql = {version = "3.
|
|
41
|
+
pydantic = "^2.6.1"
|
|
42
|
+
gql = { version = "3.6.0b1", extras = ["requests"] }
|
|
48
43
|
|
|
49
44
|
[tool.poetry.group.dev.dependencies]
|
|
50
45
|
|
|
@@ -53,10 +48,18 @@ black = "=22.12.0"
|
|
|
53
48
|
isort = "^5.11.4"
|
|
54
49
|
|
|
55
50
|
# Linters
|
|
56
|
-
mypy = "*"
|
|
57
51
|
pydocstyle = "^6.3.0"
|
|
58
52
|
pylint = "^2.16.2"
|
|
59
53
|
|
|
54
|
+
# FIXME: Upgrading mypy will require updates to aliased fields. e.g.
|
|
55
|
+
#
|
|
56
|
+
# class EtchSigner(BaseModel):
|
|
57
|
+
# redirect_url: Optional[str] = Field(None, alias="redirectURL")
|
|
58
|
+
#
|
|
59
|
+
# Not sure what the solution is.
|
|
60
|
+
mypy = "1.0.1"
|
|
61
|
+
|
|
62
|
+
|
|
60
63
|
# Testing
|
|
61
64
|
pytest = "^7.2.1"
|
|
62
65
|
pytest-cov = "^4.0.0"
|
|
@@ -74,7 +77,7 @@ pygments = "^2.14.0"
|
|
|
74
77
|
# Tooling
|
|
75
78
|
pyinstaller = "^5.8.0"
|
|
76
79
|
sniffer = "*"
|
|
77
|
-
|
|
80
|
+
macfsevents = { version = "^0.8.4", platform = "darwin" }
|
|
78
81
|
pync = { version = "*", platform = "darwin" }
|
|
79
82
|
pyinotify = {version = "^0.9.6", optional = true}
|
|
80
83
|
tox = "^3.21.2"
|
|
@@ -90,7 +93,7 @@ anvil = "python_anvil.cli:cli"
|
|
|
90
93
|
|
|
91
94
|
[tool.black]
|
|
92
95
|
|
|
93
|
-
target-version = ["
|
|
96
|
+
target-version = ["py38", "py39", "py310", "py311"]
|
|
94
97
|
skip-string-normalization = true
|
|
95
98
|
|
|
96
99
|
[build-system]
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from pkg_resources import DistributionNotFound, get_distribution
|
|
2
2
|
|
|
3
3
|
from python_anvil import api, cli
|
|
4
|
+
from python_anvil.models import FileCompatibleBaseModel
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
try:
|
|
7
8
|
__version__ = get_distribution('python_anvil').version
|
|
8
9
|
except DistributionNotFound:
|
|
9
10
|
__version__ = '(local)'
|
|
11
|
+
|
|
12
|
+
__all__ = ['api', 'cli', 'FileCompatibleBaseModel']
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from gql import gql
|
|
3
3
|
from graphql import DocumentNode
|
|
4
|
-
from typing import Any, AnyStr, Callable, Dict, List, Optional,
|
|
4
|
+
from typing import Any, AnyStr, Callable, Dict, List, Optional, Tuple, Union
|
|
5
5
|
|
|
6
6
|
from .api_resources.mutations import (
|
|
7
7
|
BaseQuery,
|
|
@@ -137,9 +137,7 @@ class Anvil:
|
|
|
137
137
|
if isinstance(payload, dict):
|
|
138
138
|
data = FillPDFPayload(**payload)
|
|
139
139
|
elif isinstance(payload, str):
|
|
140
|
-
data = FillPDFPayload.
|
|
141
|
-
payload, content_type="application/json"
|
|
142
|
-
)
|
|
140
|
+
data = FillPDFPayload.model_validate_json(payload)
|
|
143
141
|
elif isinstance(payload, FillPDFPayload):
|
|
144
142
|
data = payload
|
|
145
143
|
else:
|
|
@@ -158,7 +156,7 @@ class Anvil:
|
|
|
158
156
|
api = RestRequest(client=self.client)
|
|
159
157
|
return api.post(
|
|
160
158
|
f"fill/{template_id}.pdf",
|
|
161
|
-
data.
|
|
159
|
+
data.model_dump(by_alias=True, exclude_none=True) if data else {},
|
|
162
160
|
**kwargs,
|
|
163
161
|
)
|
|
164
162
|
|
|
@@ -169,9 +167,7 @@ class Anvil:
|
|
|
169
167
|
if isinstance(payload, dict):
|
|
170
168
|
data = GeneratePDFPayload(**payload)
|
|
171
169
|
elif isinstance(payload, str):
|
|
172
|
-
data = GeneratePDFPayload.
|
|
173
|
-
payload, content_type="application/json"
|
|
174
|
-
)
|
|
170
|
+
data = GeneratePDFPayload.model_validate_json(payload)
|
|
175
171
|
elif isinstance(payload, GeneratePDFPayload):
|
|
176
172
|
data = payload
|
|
177
173
|
else:
|
|
@@ -180,7 +176,9 @@ class Anvil:
|
|
|
180
176
|
# Any data errors would come from here
|
|
181
177
|
api = RestRequest(client=self.client)
|
|
182
178
|
return api.post(
|
|
183
|
-
"generate-pdf",
|
|
179
|
+
"generate-pdf",
|
|
180
|
+
data=data.model_dump(by_alias=True, exclude_none=True),
|
|
181
|
+
**kwargs,
|
|
184
182
|
)
|
|
185
183
|
|
|
186
184
|
def get_cast(
|
|
@@ -326,7 +324,7 @@ class Anvil:
|
|
|
326
324
|
|
|
327
325
|
return _get_return(res, get_data=get_data)
|
|
328
326
|
|
|
329
|
-
def get_weld(self, eid:
|
|
327
|
+
def get_weld(self, eid: str, **kwargs):
|
|
330
328
|
res = self.query(
|
|
331
329
|
gql(
|
|
332
330
|
"""
|
|
@@ -380,9 +378,7 @@ class Anvil:
|
|
|
380
378
|
raise TypeError('One of the arguments `payload` or `json` must exist')
|
|
381
379
|
|
|
382
380
|
if json:
|
|
383
|
-
payload = CreateEtchPacketPayload.
|
|
384
|
-
json, content_type="application/json"
|
|
385
|
-
)
|
|
381
|
+
payload = CreateEtchPacketPayload.model_validate_json(json)
|
|
386
382
|
|
|
387
383
|
if isinstance(payload, dict):
|
|
388
384
|
mutation = CreateEtchPacket.create_from_dict(payload)
|
|
@@ -396,7 +392,7 @@ class Anvil:
|
|
|
396
392
|
)
|
|
397
393
|
|
|
398
394
|
payload = mutation.create_payload()
|
|
399
|
-
variables = payload.
|
|
395
|
+
variables = payload.model_dump(by_alias=True, exclude_none=True)
|
|
400
396
|
|
|
401
397
|
return self.mutate(mutation, variables=variables, upload_files=True, **kwargs)
|
|
402
398
|
|
|
@@ -407,7 +403,9 @@ class Anvil:
|
|
|
407
403
|
client_user_id=client_user_id,
|
|
408
404
|
)
|
|
409
405
|
payload = mutation.create_payload()
|
|
410
|
-
return self.mutate(
|
|
406
|
+
return self.mutate(
|
|
407
|
+
mutation, variables=payload.model_dump(by_alias=True), **kwargs
|
|
408
|
+
)
|
|
411
409
|
|
|
412
410
|
def download_documents(self, document_group_eid: str, **kwargs):
|
|
413
411
|
"""Retrieve all completed documents in zip form."""
|
|
@@ -416,7 +414,7 @@ class Anvil:
|
|
|
416
414
|
|
|
417
415
|
def forge_submit(
|
|
418
416
|
self,
|
|
419
|
-
payload: Optional[Union[Dict[
|
|
417
|
+
payload: Optional[Union[Dict[str, Any], ForgeSubmitPayload]] = None,
|
|
420
418
|
json=None,
|
|
421
419
|
**kwargs,
|
|
422
420
|
) -> Dict[str, Any]:
|
|
@@ -425,9 +423,7 @@ class Anvil:
|
|
|
425
423
|
raise TypeError('One of arguments `json` or `payload` are required')
|
|
426
424
|
|
|
427
425
|
if json:
|
|
428
|
-
payload = ForgeSubmitPayload.
|
|
429
|
-
json, content_type="application/json"
|
|
430
|
-
)
|
|
426
|
+
payload = ForgeSubmitPayload.model_validate_json(json)
|
|
431
427
|
|
|
432
428
|
if isinstance(payload, dict):
|
|
433
429
|
mutation = ForgeSubmit.create_from_dict(payload)
|
|
@@ -440,6 +436,8 @@ class Anvil:
|
|
|
440
436
|
|
|
441
437
|
return self.mutate(
|
|
442
438
|
mutation,
|
|
443
|
-
variables=mutation.create_payload().
|
|
439
|
+
variables=mutation.create_payload().model_dump(
|
|
440
|
+
by_alias=True, exclude_none=True
|
|
441
|
+
),
|
|
444
442
|
**kwargs,
|
|
445
443
|
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# pylint: disable=no-self-argument
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
# Disabling pylint no-name-in-module because this is the documented way to
|
|
5
|
+
# import `BaseModel` and it's not broken, so let's keep it.
|
|
6
|
+
from pydantic import ( # pylint: disable=no-name-in-module
|
|
7
|
+
BaseModel as _BaseModel,
|
|
8
|
+
ConfigDict,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
under_pat = re.compile(r"_([a-z])")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def underscore_to_camel(name):
|
|
16
|
+
ret = under_pat.sub(lambda x: x.group(1).upper(), name)
|
|
17
|
+
return ret
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseModel(_BaseModel):
|
|
21
|
+
"""Config override for all models.
|
|
22
|
+
|
|
23
|
+
This override is mainly so everything can go from snake to camel-case.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Allow extra fields even if it is not defined. This will allow models
|
|
27
|
+
# to be more flexible if features are added in the Anvil API, but
|
|
28
|
+
# explicit support hasn't been added yet to this library.
|
|
29
|
+
model_config = ConfigDict(
|
|
30
|
+
alias_generator=underscore_to_camel, populate_by_name=True, extra="allow"
|
|
31
|
+
)
|
{python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/create_etch_packet.py
RENAMED
|
@@ -19,17 +19,14 @@ from python_anvil.utils import create_unique_id
|
|
|
19
19
|
logger: Logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
21
|
DEFAULT_RESPONSE_QUERY = """{
|
|
22
|
-
id
|
|
23
22
|
eid
|
|
24
23
|
name
|
|
25
24
|
detailsURL
|
|
26
25
|
documentGroup {
|
|
27
|
-
id
|
|
28
26
|
eid
|
|
29
27
|
status
|
|
30
28
|
files
|
|
31
29
|
signers {
|
|
32
|
-
id
|
|
33
30
|
eid
|
|
34
31
|
aliasId
|
|
35
32
|
routingOrder
|
{python_anvil-3.0.1 → python_anvil-5.0.0}/python_anvil/api_resources/mutations/forge_submit.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Dict, Optional,
|
|
1
|
+
from typing import Any, Dict, Optional, Union
|
|
2
2
|
|
|
3
3
|
from python_anvil.api_resources.mutations.base import BaseQuery
|
|
4
4
|
from python_anvil.api_resources.mutations.helpers import get_payload_attrs
|
|
@@ -7,7 +7,6 @@ from python_anvil.api_resources.payload import ForgeSubmitPayload
|
|
|
7
7
|
|
|
8
8
|
DEFAULT_RESPONSE_QUERY = """
|
|
9
9
|
{
|
|
10
|
-
id
|
|
11
10
|
eid
|
|
12
11
|
status
|
|
13
12
|
resolvedPayload
|
|
@@ -22,7 +21,6 @@ DEFAULT_RESPONSE_QUERY = """
|
|
|
22
21
|
routingOrder
|
|
23
22
|
}
|
|
24
23
|
weldData {
|
|
25
|
-
id
|
|
26
24
|
eid
|
|
27
25
|
status
|
|
28
26
|
isTest
|
|
@@ -72,10 +70,10 @@ class ForgeSubmit(BaseQuery):
|
|
|
72
70
|
|
|
73
71
|
def __init__(
|
|
74
72
|
self,
|
|
75
|
-
payload: Union[Dict[
|
|
76
|
-
forge_eid: Optional[
|
|
77
|
-
weld_data_eid: Optional[
|
|
78
|
-
submission_eid: Optional[
|
|
73
|
+
payload: Union[Dict[str, Any], ForgeSubmitPayload],
|
|
74
|
+
forge_eid: Optional[str] = None,
|
|
75
|
+
weld_data_eid: Optional[str] = None,
|
|
76
|
+
submission_eid: Optional[str] = None,
|
|
79
77
|
is_test: Optional[bool] = None,
|
|
80
78
|
**kwargs,
|
|
81
79
|
):
|
|
@@ -109,7 +107,7 @@ class ForgeSubmit(BaseQuery):
|
|
|
109
107
|
setattr(self, attr, kwargs[attr])
|
|
110
108
|
|
|
111
109
|
@classmethod
|
|
112
|
-
def create_from_dict(cls, payload: Dict[
|
|
110
|
+
def create_from_dict(cls, payload: Dict[str, Any]):
|
|
113
111
|
# Parse the data through the model class to validate and pass it back
|
|
114
112
|
# as variables in this class.
|
|
115
113
|
return cls(**payload)
|
|
@@ -6,12 +6,14 @@ from io import BufferedIOBase
|
|
|
6
6
|
# Disabling pylint no-name-in-module because this is the documented way to
|
|
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
|
+
ConfigDict,
|
|
9
10
|
Field,
|
|
10
11
|
HttpUrl,
|
|
11
|
-
|
|
12
|
-
validator,
|
|
12
|
+
field_validator,
|
|
13
13
|
)
|
|
14
|
-
from typing import Any, Dict, List, Optional,
|
|
14
|
+
from typing import Any, Dict, List, Optional, Union
|
|
15
|
+
|
|
16
|
+
from python_anvil.models import FileCompatibleBaseModel
|
|
15
17
|
|
|
16
18
|
from .base import BaseModel
|
|
17
19
|
|
|
@@ -24,8 +26,8 @@ else:
|
|
|
24
26
|
|
|
25
27
|
class EmbeddedLogo(BaseModel):
|
|
26
28
|
src: str
|
|
27
|
-
max_width: Optional[int]
|
|
28
|
-
max_height: Optional[int]
|
|
29
|
+
max_width: Optional[int] = None
|
|
30
|
+
max_height: Optional[int] = None
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class FillPDFPayload(BaseModel):
|
|
@@ -34,7 +36,8 @@ class FillPDFPayload(BaseModel):
|
|
|
34
36
|
font_size: Optional[int] = None
|
|
35
37
|
text_color: Optional[str] = None
|
|
36
38
|
|
|
37
|
-
@
|
|
39
|
+
@field_validator("data")
|
|
40
|
+
@classmethod
|
|
38
41
|
def data_cannot_be_empty(cls, v):
|
|
39
42
|
if isinstance(v, dict) and len(v) == 0:
|
|
40
43
|
raise ValueError("cannot be empty")
|
|
@@ -43,25 +46,13 @@ class FillPDFPayload(BaseModel):
|
|
|
43
46
|
|
|
44
47
|
class GeneratePDFPayload(BaseModel):
|
|
45
48
|
data: Union[List[Dict[str, Any]], Dict[Literal["html", "css"], str]]
|
|
46
|
-
logo: Optional[EmbeddedLogo]
|
|
47
|
-
title: Optional[str]
|
|
49
|
+
logo: Optional[EmbeddedLogo] = None
|
|
50
|
+
title: Optional[str] = None
|
|
48
51
|
type: Optional[Literal["markdown", "html"]] = "markdown"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if "html" not in v:
|
|
54
|
-
raise ValueError("must contain HTML if using `html` type")
|
|
55
|
-
if isinstance(v, list):
|
|
56
|
-
raise ValueError("must contain a data dict not a list")
|
|
57
|
-
return v
|
|
58
|
-
|
|
59
|
-
@validator("type")
|
|
60
|
-
def type_must_match_data(cls, v, values):
|
|
61
|
-
if v == "html":
|
|
62
|
-
if "data" in values and not isinstance(values["data"], dict):
|
|
63
|
-
raise ValueError("HTML types must used a dict data payload")
|
|
64
|
-
return v
|
|
52
|
+
page: Optional[Dict[str, Any]] = None
|
|
53
|
+
font_size: Optional[int] = None
|
|
54
|
+
font_family: Optional[str] = None
|
|
55
|
+
text_color: Optional[str] = None
|
|
65
56
|
|
|
66
57
|
|
|
67
58
|
class GenerateEtchSigningURLPayload(BaseModel):
|
|
@@ -178,7 +169,7 @@ class DocumentMarkdown(BaseModel):
|
|
|
178
169
|
text_color: str = "#000000"
|
|
179
170
|
|
|
180
171
|
|
|
181
|
-
class DocumentUpload(
|
|
172
|
+
class DocumentUpload(FileCompatibleBaseModel):
|
|
182
173
|
"""Dataclass representing an uploaded document."""
|
|
183
174
|
|
|
184
175
|
id: str
|
|
@@ -189,16 +180,14 @@ class DocumentUpload(BaseModel):
|
|
|
189
180
|
# the client library side.
|
|
190
181
|
# This might be a bug on the `pydantic` side(?) when this object gets
|
|
191
182
|
# converted into a dict.
|
|
192
|
-
|
|
183
|
+
|
|
184
|
+
# NOTE: This field name is referenced in the models.py file, if you change it you
|
|
185
|
+
# must change the reference
|
|
186
|
+
file: Any = None
|
|
193
187
|
fields: List[SignatureField]
|
|
194
188
|
font_size: int = 14
|
|
195
189
|
text_color: str = "#000000"
|
|
196
|
-
|
|
197
|
-
class Config:
|
|
198
|
-
"""Special rules for this model class."""
|
|
199
|
-
|
|
200
|
-
# Needed to allow `BufferedIOBase` as an allowed `file` type.
|
|
201
|
-
arbitrary_types_allowed = True
|
|
190
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
202
191
|
|
|
203
192
|
|
|
204
193
|
class EtchCastRef(BaseModel):
|
|
@@ -212,7 +201,7 @@ class CreateEtchFilePayload(BaseModel):
|
|
|
212
201
|
payloads: Union[str, Dict[str, FillPDFPayload]]
|
|
213
202
|
|
|
214
203
|
|
|
215
|
-
class CreateEtchPacketPayload(
|
|
204
|
+
class CreateEtchPacketPayload(FileCompatibleBaseModel):
|
|
216
205
|
"""
|
|
217
206
|
Payload for createEtchPacket.
|
|
218
207
|
|
|
@@ -222,6 +211,9 @@ class CreateEtchPacketPayload(BaseModel):
|
|
|
222
211
|
|
|
223
212
|
name: str
|
|
224
213
|
signers: List[EtchSigner]
|
|
214
|
+
# NOTE: This is a list of `AttachableEtchFile` objects, but we need to
|
|
215
|
+
# override the default `FileCompatibleBaseModel` to handle multipart/form-data
|
|
216
|
+
# uploads correctly. This field name is referenced in the models.py file.
|
|
225
217
|
files: List["AttachableEtchFile"]
|
|
226
218
|
signature_email_subject: Optional[str] = None
|
|
227
219
|
signature_email_body: Optional[str] = None
|
|
@@ -246,10 +238,10 @@ class ForgeSubmitPayload(BaseModel):
|
|
|
246
238
|
https://www.useanvil.com/docs/api/graphql/reference/#operation-forgesubmit-Mutations
|
|
247
239
|
"""
|
|
248
240
|
|
|
249
|
-
forge_eid:
|
|
250
|
-
payload: Dict[
|
|
251
|
-
weld_data_eid: Optional[
|
|
252
|
-
submission_eid: Optional[
|
|
241
|
+
forge_eid: str
|
|
242
|
+
payload: Dict[str, Any]
|
|
243
|
+
weld_data_eid: Optional[str] = None
|
|
244
|
+
submission_eid: Optional[str] = None
|
|
253
245
|
# Defaults to True when not provided/is None
|
|
254
246
|
enforce_payload_valid_on_create: Optional[bool] = None
|
|
255
247
|
current_step: Optional[int] = None
|
|
@@ -257,22 +249,11 @@ class ForgeSubmitPayload(BaseModel):
|
|
|
257
249
|
# Note that if using a development API key, this will be forced to `True`
|
|
258
250
|
# even when `False` is used in the payload.
|
|
259
251
|
is_test: Optional[bool] = True
|
|
260
|
-
timezone: Optional[
|
|
252
|
+
timezone: Optional[str] = None
|
|
261
253
|
webhook_url: Optional[HttpUrl] = Field(None, alias="webhookURL")
|
|
262
|
-
group_array_id: Optional[
|
|
254
|
+
group_array_id: Optional[str] = None
|
|
263
255
|
group_array_index: Optional[int] = None
|
|
264
256
|
|
|
265
|
-
@root_validator
|
|
266
|
-
def wd_submission_both_required(cls, values):
|
|
267
|
-
both_required = ["weld_data_eid", "submission_eid"]
|
|
268
|
-
picked = [k for k in both_required if k in values and values[k] is not None]
|
|
269
|
-
if len(picked) == 1:
|
|
270
|
-
raise ValueError(
|
|
271
|
-
"Both `weld_data_eid` and `submission_eid` are "
|
|
272
|
-
"required if either are provided."
|
|
273
|
-
)
|
|
274
|
-
return values
|
|
275
|
-
|
|
276
257
|
|
|
277
258
|
UploadableFile = Union[Base64Upload, BufferedIOBase]
|
|
278
259
|
AttachableEtchFile = Union[
|
|
@@ -282,5 +263,5 @@ AttachableEtchFile = Union[
|
|
|
282
263
|
# Classes below use types wrapped in quotes avoid a circular dependency/weird
|
|
283
264
|
# variable assignment locations with the aliases above. We need to manually
|
|
284
265
|
# update the refs for them to point to the right things.
|
|
285
|
-
DocumentUpload.
|
|
286
|
-
CreateEtchPacketPayload.
|
|
266
|
+
DocumentUpload.model_rebuild()
|
|
267
|
+
CreateEtchPacketPayload.model_rebuild()
|
|
@@ -208,7 +208,7 @@ def fill_pdf(ctx, template_id, out_filename, payload_csv):
|
|
|
208
208
|
with click.progressbar(payloads, label="Filling PDFs and saving") as ps:
|
|
209
209
|
indexed_files = utils.build_batch_filenames(out_filename)
|
|
210
210
|
for payload in ps:
|
|
211
|
-
res = anvil.fill_pdf(template_id, payload.
|
|
211
|
+
res = anvil.fill_pdf(template_id, payload.model_dump(), debug=debug)
|
|
212
212
|
|
|
213
213
|
if contains_headers(res):
|
|
214
214
|
res, headers = process_response(res)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
|
+
|
|
3
|
+
# import json
|
|
2
4
|
import requests
|
|
3
5
|
from base64 import b64encode
|
|
4
6
|
from gql import Client
|
|
@@ -53,6 +55,15 @@ def get_gql_ds(client: Client) -> DSLSchema:
|
|
|
53
55
|
return DSLSchema(client.schema)
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
# FIXME: when gql 3.6.0 is stable, we can use this to paper over pre-graphql
|
|
59
|
+
# handler errors. https://github.com/graphql-python/gql/releases/tag/v3.6.0b1
|
|
60
|
+
# def json_deserialize (response_text):
|
|
61
|
+
# try:
|
|
62
|
+
# return json.loads(response_text)
|
|
63
|
+
# except Exception as e:
|
|
64
|
+
# return {"errors": [{"message": response_text}]}
|
|
65
|
+
|
|
66
|
+
|
|
56
67
|
class GQLClient:
|
|
57
68
|
"""GraphQL client factory class."""
|
|
58
69
|
|
|
@@ -71,6 +82,8 @@ class GQLClient:
|
|
|
71
82
|
auth=auth,
|
|
72
83
|
url=endpoint_url,
|
|
73
84
|
verify=True,
|
|
85
|
+
# FIXME: when gql 3.6.0 is stable... see note above
|
|
86
|
+
# json_deserialize=json_deserialize,
|
|
74
87
|
)
|
|
75
88
|
|
|
76
89
|
schema = None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
from io import BufferedReader, BytesIO
|
|
4
|
+
from mimetypes import guess_type
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
from python_anvil.api_resources.base import underscore_to_camel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileCompatibleBaseModel(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Patched model_dump to extract file objects from SerializationIterator.
|
|
13
|
+
|
|
14
|
+
Becaus of Pydantic V2. Return as BufferedReader or base64 encoded dict as needed.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Allow extra fields even if it is not defined. This will allow models
|
|
18
|
+
# to be more flexible if features are added in the Anvil API, but
|
|
19
|
+
# explicit support hasn't been added yet to this library.
|
|
20
|
+
model_config = ConfigDict(
|
|
21
|
+
alias_generator=underscore_to_camel, populate_by_name=True, extra="allow"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _iterator_to_buffered_reader(self, value):
|
|
25
|
+
content = bytearray()
|
|
26
|
+
try:
|
|
27
|
+
while True:
|
|
28
|
+
content.extend(next(value))
|
|
29
|
+
except StopIteration:
|
|
30
|
+
# Create a BytesIO with the content
|
|
31
|
+
bio = BytesIO(bytes(content))
|
|
32
|
+
# Create a BufferedReader with the content
|
|
33
|
+
reader = BufferedReader(bio) # type: ignore[arg-type]
|
|
34
|
+
return reader
|
|
35
|
+
|
|
36
|
+
def _check_if_serialization_iterator(self, value):
|
|
37
|
+
return str(type(value).__name__) == 'SerializationIterator' and hasattr(
|
|
38
|
+
value, '__next__'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _process_file_data(self, file_obj):
|
|
42
|
+
"""Process file object into base64 encoded dict format."""
|
|
43
|
+
# Read the file data and encode it as base64
|
|
44
|
+
file_content = file_obj.read()
|
|
45
|
+
|
|
46
|
+
# Get filename - handle both regular files and BytesIO objects
|
|
47
|
+
filename = getattr(file_obj, 'name', "document.pdf")
|
|
48
|
+
|
|
49
|
+
if isinstance(filename, (bytes, bytearray)):
|
|
50
|
+
filename = filename.decode('utf-8')
|
|
51
|
+
|
|
52
|
+
# manage mimetype based on file extension
|
|
53
|
+
mimetype = guess_type(filename)[0] or 'application/pdf'
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
'data': base64.b64encode(file_content).decode('utf-8'),
|
|
57
|
+
'mimetype': mimetype,
|
|
58
|
+
'filename': os.path.basename(filename),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def model_dump(self, **kwargs):
|
|
62
|
+
data = super().model_dump(**kwargs)
|
|
63
|
+
for key, value in data.items():
|
|
64
|
+
if key == 'file' and self._check_if_serialization_iterator(value):
|
|
65
|
+
# Direct file case
|
|
66
|
+
file_obj = self._iterator_to_buffered_reader(value)
|
|
67
|
+
data[key] = self._process_file_data(file_obj)
|
|
68
|
+
elif key == 'files' and isinstance(value, list):
|
|
69
|
+
# List of objects case
|
|
70
|
+
for index, item in enumerate(value):
|
|
71
|
+
if isinstance(item, dict) and 'file' in item:
|
|
72
|
+
if self._check_if_serialization_iterator(item['file']):
|
|
73
|
+
file_obj = self._iterator_to_buffered_reader(item['file'])
|
|
74
|
+
data[key][index]['file'] = self._process_file_data(file_obj)
|
|
75
|
+
return data
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# pylint: disable=unused-variable,unused-argument,too-many-statements
|
|
2
2
|
import json
|
|
3
3
|
import pytest
|
|
4
|
-
from pydantic import ValidationError
|
|
5
4
|
from typing import Any, MutableMapping
|
|
6
5
|
from unittest import mock
|
|
7
6
|
|
|
@@ -186,18 +185,6 @@ def describe_api():
|
|
|
186
185
|
)
|
|
187
186
|
assert m_request_post.call_count == 0
|
|
188
187
|
|
|
189
|
-
@mock.patch('python_anvil.api.RestRequest.post')
|
|
190
|
-
def test_invalid_data_for_html(m_request_post, anvil):
|
|
191
|
-
with pytest.raises(ValueError):
|
|
192
|
-
anvil.generate_pdf(
|
|
193
|
-
{
|
|
194
|
-
# This should be a plain dict, not a list
|
|
195
|
-
"data": [{"d1": "data"}],
|
|
196
|
-
"type": "html",
|
|
197
|
-
}
|
|
198
|
-
)
|
|
199
|
-
assert m_request_post.call_count == 0
|
|
200
|
-
|
|
201
188
|
@mock.patch('python_anvil.api.RestRequest.post')
|
|
202
189
|
def test_invalid_data_for_markdown(m_request_post, anvil):
|
|
203
190
|
with pytest.raises(ValueError):
|
|
@@ -355,13 +342,13 @@ def describe_api():
|
|
|
355
342
|
for k, v in payloads.EXPECTED_ETCH_TEST_PAYLOAD_JSON.items()
|
|
356
343
|
if v is not None
|
|
357
344
|
}
|
|
358
|
-
cep = CreateEtchPacketPayload.
|
|
345
|
+
cep = CreateEtchPacketPayload.model_validate(payload)
|
|
359
346
|
cep.newFeature = True # type: ignore[attr-defined]
|
|
360
347
|
anvil.create_etch_packet(payload=cep)
|
|
361
348
|
assert m_request_post.call_count == 1
|
|
362
349
|
|
|
363
350
|
variables = m_request_post.call_args[1]["variable_values"]
|
|
364
|
-
assert cep.
|
|
351
|
+
assert cep.model_dump(by_alias=True, exclude_none=True) == variables
|
|
365
352
|
assert "newFeature" in variables
|
|
366
353
|
|
|
367
354
|
def describe_forge_submit():
|
|
@@ -386,30 +373,6 @@ def describe_api():
|
|
|
386
373
|
assert m_request_post.call_count == 1
|
|
387
374
|
assert {"variable_values": expected_data} in m_request_post.call_args
|
|
388
375
|
|
|
389
|
-
@mock.patch('gql.Client.execute')
|
|
390
|
-
def test_invalid_wd_submission(m_request_post, anvil):
|
|
391
|
-
with pytest.raises(ValidationError):
|
|
392
|
-
payload = dict(
|
|
393
|
-
forge_eid="forge1234",
|
|
394
|
-
# weld_data_eid and submission_eid must be provided
|
|
395
|
-
# if one exists.
|
|
396
|
-
weld_data_eid="wd1234",
|
|
397
|
-
payload=dict(field1="Updated data"),
|
|
398
|
-
)
|
|
399
|
-
anvil.forge_submit(payload=payload)
|
|
400
|
-
assert m_request_post.call_count == 0
|
|
401
|
-
|
|
402
|
-
with pytest.raises(ValidationError):
|
|
403
|
-
payload = dict(
|
|
404
|
-
forge_eid="forge1234",
|
|
405
|
-
# weld_data_eid and submission_eid must be provided
|
|
406
|
-
# if one exists.
|
|
407
|
-
submission_eid="sub1234",
|
|
408
|
-
payload=dict(field1="Updated data"),
|
|
409
|
-
)
|
|
410
|
-
anvil.forge_submit(payload=payload)
|
|
411
|
-
assert m_request_post.call_count == 0
|
|
412
|
-
|
|
413
376
|
@mock.patch('gql.Client.execute')
|
|
414
377
|
def test_minimum_valid_data_submission(m_request_post, anvil):
|
|
415
378
|
payload = ForgeSubmitPayload(
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import pytest
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from python_anvil.models import FileCompatibleBaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_file_compat_base_model_handles_regular_data():
|
|
10
|
+
class TestModel(FileCompatibleBaseModel):
|
|
11
|
+
name: str
|
|
12
|
+
value: int
|
|
13
|
+
|
|
14
|
+
model = TestModel(name="test", value=42)
|
|
15
|
+
data = model.model_dump()
|
|
16
|
+
assert data == {"name": "test", "value": 42}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_file_compat_base_model_preserves_file_objects():
|
|
20
|
+
class FileModel(FileCompatibleBaseModel):
|
|
21
|
+
file: Any = None
|
|
22
|
+
|
|
23
|
+
# Create a test file object
|
|
24
|
+
with open(__file__, 'rb') as test_file:
|
|
25
|
+
model = FileModel(file=test_file)
|
|
26
|
+
data = model.model_dump()
|
|
27
|
+
|
|
28
|
+
# Verify we got a dictionary with the expected structure
|
|
29
|
+
assert isinstance(data['file'], dict)
|
|
30
|
+
assert 'data' in data['file']
|
|
31
|
+
assert 'mimetype' in data['file']
|
|
32
|
+
assert 'filename' in data['file']
|
|
33
|
+
|
|
34
|
+
# Verify the content matches
|
|
35
|
+
with open(__file__, 'rb') as original_file:
|
|
36
|
+
original_content = original_file.read()
|
|
37
|
+
decoded_content = base64.b64decode(data['file']['data'].encode('utf-8'))
|
|
38
|
+
assert (
|
|
39
|
+
decoded_content == original_content
|
|
40
|
+
), "File content should match original"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_file_compat_base_model_validates_types():
|
|
44
|
+
class TestModel(FileCompatibleBaseModel):
|
|
45
|
+
name: str
|
|
46
|
+
age: int
|
|
47
|
+
|
|
48
|
+
# Should work with valid types
|
|
49
|
+
model = TestModel(name="Alice", age=30)
|
|
50
|
+
assert model.name == "Alice"
|
|
51
|
+
assert model.age == 30
|
|
52
|
+
|
|
53
|
+
# Should raise validation error for wrong types
|
|
54
|
+
with pytest.raises(ValueError):
|
|
55
|
+
TestModel(name="Alice", age="thirty")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_file_compat_base_model_handles_optional_fields():
|
|
59
|
+
class TestModel(FileCompatibleBaseModel):
|
|
60
|
+
required: str
|
|
61
|
+
optional: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
# Should work with just required field
|
|
64
|
+
model = TestModel(required="test")
|
|
65
|
+
assert model.required == "test"
|
|
66
|
+
assert model.optional is None
|
|
67
|
+
|
|
68
|
+
# Should work with both fields
|
|
69
|
+
model = TestModel(required="test", optional="present")
|
|
70
|
+
assert model.optional == "present"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_file_compat_base_model_handles_nested_models():
|
|
74
|
+
class NestedModel(BaseModel):
|
|
75
|
+
value: str
|
|
76
|
+
|
|
77
|
+
class ParentModel(FileCompatibleBaseModel):
|
|
78
|
+
nested: NestedModel
|
|
79
|
+
|
|
80
|
+
nested = NestedModel(value="test")
|
|
81
|
+
model = ParentModel(nested=nested)
|
|
82
|
+
|
|
83
|
+
data = model.model_dump()
|
|
84
|
+
assert data == {"nested": {"value": "test"}}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_file_compat_base_model_handles_lists():
|
|
88
|
+
class TestModel(FileCompatibleBaseModel):
|
|
89
|
+
items: List[str]
|
|
90
|
+
|
|
91
|
+
model = TestModel(items=["a", "b", "c"])
|
|
92
|
+
data = model.model_dump()
|
|
93
|
+
assert data == {"items": ["a", "b", "c"]}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_document_upload_handles_file_objects():
|
|
97
|
+
# pylint: disable-next=import-outside-toplevel
|
|
98
|
+
from python_anvil.api_resources.payload import DocumentUpload, SignatureField
|
|
99
|
+
|
|
100
|
+
# Create a sample signature field
|
|
101
|
+
field = SignatureField(
|
|
102
|
+
id="sig1",
|
|
103
|
+
type="signature",
|
|
104
|
+
page_num=1,
|
|
105
|
+
rect={"x": 100.0, "y": 100.0, "width": 100.0},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Test with a file object
|
|
109
|
+
with open(__file__, 'rb') as test_file:
|
|
110
|
+
doc = DocumentUpload(
|
|
111
|
+
id="doc1", title="Test Document", file=test_file, fields=[field]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
data = doc.model_dump()
|
|
115
|
+
|
|
116
|
+
# Verify file is converted to expected dictionary format
|
|
117
|
+
assert isinstance(data['file'], dict)
|
|
118
|
+
assert 'data' in data['file']
|
|
119
|
+
assert 'mimetype' in data['file']
|
|
120
|
+
assert 'filename' in data['file']
|
|
121
|
+
|
|
122
|
+
# Verify content matches
|
|
123
|
+
with open(__file__, 'rb') as original_file:
|
|
124
|
+
original_content = original_file.read()
|
|
125
|
+
decoded_content = base64.b64decode(data['file']['data'].encode('utf-8'))
|
|
126
|
+
assert decoded_content == original_content
|
|
127
|
+
|
|
128
|
+
# Verify other fields are correct
|
|
129
|
+
assert data['id'] == "doc1"
|
|
130
|
+
assert data['title'] == "Test Document"
|
|
131
|
+
assert len(data['fields']) == 1
|
|
132
|
+
assert data['fields'][0]['id'] == "sig1"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_create_etch_packet_payload_handles_nested_file_objects():
|
|
136
|
+
# pylint: disable-next=import-outside-toplevel
|
|
137
|
+
from python_anvil.api_resources.payload import (
|
|
138
|
+
CreateEtchPacketPayload,
|
|
139
|
+
DocumentUpload,
|
|
140
|
+
EtchSigner,
|
|
141
|
+
SignatureField,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Create a sample signature field
|
|
145
|
+
field = SignatureField(
|
|
146
|
+
id="sig1",
|
|
147
|
+
type="signature",
|
|
148
|
+
page_num=1,
|
|
149
|
+
rect={"x": 100.0, "y": 100.0, "width": 100.0},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Create a signer
|
|
153
|
+
signer = EtchSigner(
|
|
154
|
+
name="Test Signer",
|
|
155
|
+
email="test@example.com",
|
|
156
|
+
fields=[{"file_id": "doc1", "field_id": "sig1"}],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Test with a file object
|
|
160
|
+
with open(__file__, 'rb') as test_file:
|
|
161
|
+
# Create a DocumentUpload instance
|
|
162
|
+
doc = DocumentUpload(
|
|
163
|
+
id="doc1", title="Test Document", file=test_file, fields=[field]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Create the packet payload
|
|
167
|
+
packet = CreateEtchPacketPayload(
|
|
168
|
+
name="Test Packet", signers=[signer], files=[doc], is_test=True
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Dump the model
|
|
172
|
+
data = packet.model_dump()
|
|
173
|
+
|
|
174
|
+
# Verify the structure
|
|
175
|
+
assert data['name'] == "Test Packet"
|
|
176
|
+
assert len(data['files']) == 1
|
|
177
|
+
assert len(data['signers']) == 1
|
|
178
|
+
|
|
179
|
+
# Verify file handling in the nested DocumentUpload
|
|
180
|
+
file_data = data['files'][0]
|
|
181
|
+
assert file_data['id'] == "doc1"
|
|
182
|
+
assert file_data['title'] == "Test Document"
|
|
183
|
+
assert isinstance(file_data['file'], dict)
|
|
184
|
+
assert 'data' in file_data['file']
|
|
185
|
+
assert 'mimetype' in file_data['file']
|
|
186
|
+
assert 'filename' in file_data['file']
|
|
187
|
+
|
|
188
|
+
# Verify content matches
|
|
189
|
+
with open(__file__, 'rb') as original_file:
|
|
190
|
+
original_content = original_file.read()
|
|
191
|
+
decoded_content = base64.b64decode(
|
|
192
|
+
file_data['file']['data'].encode('utf-8')
|
|
193
|
+
)
|
|
194
|
+
assert decoded_content == original_content
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_create_etch_packet_payload_handles_multiple_files():
|
|
198
|
+
# pylint: disable-next=import-outside-toplevel
|
|
199
|
+
from python_anvil.api_resources.payload import (
|
|
200
|
+
CreateEtchPacketPayload,
|
|
201
|
+
DocumentUpload,
|
|
202
|
+
EtchSigner,
|
|
203
|
+
SignatureField,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Create signature fields
|
|
207
|
+
field1 = SignatureField(
|
|
208
|
+
id="sig1",
|
|
209
|
+
type="signature",
|
|
210
|
+
page_num=1,
|
|
211
|
+
rect={"x": 100.0, "y": 100.0, "width": 100.0},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
field2 = SignatureField(
|
|
215
|
+
id="sig2",
|
|
216
|
+
type="signature",
|
|
217
|
+
page_num=1,
|
|
218
|
+
rect={"x": 200.0, "y": 200.0, "width": 100.0},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
signer = EtchSigner(
|
|
222
|
+
name="Test Signer",
|
|
223
|
+
email="test@example.com",
|
|
224
|
+
fields=[
|
|
225
|
+
{"file_id": "doc1", "field_id": "sig1"},
|
|
226
|
+
{"file_id": "doc2", "field_id": "sig2"},
|
|
227
|
+
],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Test with multiple file objects
|
|
231
|
+
with open(__file__, 'rb') as test_file1, open(__file__, 'rb') as test_file2:
|
|
232
|
+
doc1 = DocumentUpload(
|
|
233
|
+
id="doc1", title="Test Document 1", file=test_file1, fields=[field1]
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
doc2 = DocumentUpload(
|
|
237
|
+
id="doc2", title="Test Document 2", file=test_file2, fields=[field2]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
packet = CreateEtchPacketPayload(
|
|
241
|
+
name="Test Packet", signers=[signer], files=[doc1, doc2], is_test=True
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
data = packet.model_dump()
|
|
245
|
+
|
|
246
|
+
# Verify structure
|
|
247
|
+
assert len(data['files']) == 2
|
|
248
|
+
|
|
249
|
+
# Verify both files are properly handled
|
|
250
|
+
for i, file_data in enumerate(data['files'], 1):
|
|
251
|
+
assert file_data['id'] == f"doc{i}"
|
|
252
|
+
assert file_data['title'] == f"Test Document {i}"
|
|
253
|
+
assert isinstance(file_data['file'], dict)
|
|
254
|
+
assert 'data' in file_data['file']
|
|
255
|
+
assert 'mimetype' in file_data['file']
|
|
256
|
+
assert 'filename' in file_data['file']
|
|
257
|
+
|
|
258
|
+
# Verify content matches
|
|
259
|
+
with open(__file__, 'rb') as original_file:
|
|
260
|
+
original_content = original_file.read()
|
|
261
|
+
decoded_content = base64.b64decode(
|
|
262
|
+
file_data['file']['data'].encode('utf-8')
|
|
263
|
+
)
|
|
264
|
+
assert decoded_content == original_content
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# pylint: disable=no-self-argument
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
# Disabling pylint no-name-in-module because this is the documented way to
|
|
5
|
-
# import `BaseModel` and it's not broken, so let's keep it.
|
|
6
|
-
from pydantic import BaseModel as _BaseModel, Extra # pylint: disable=no-name-in-module
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
under_pat = re.compile(r"_([a-z])")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def underscore_to_camel(name):
|
|
13
|
-
ret = under_pat.sub(lambda x: x.group(1).upper(), name)
|
|
14
|
-
return ret
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class BaseModel(_BaseModel):
|
|
18
|
-
class Config:
|
|
19
|
-
"""Config override for all models.
|
|
20
|
-
|
|
21
|
-
This override is mainly so everything can go from snake to camel-case.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
alias_generator = underscore_to_camel
|
|
25
|
-
allow_population_by_field_name = True
|
|
26
|
-
|
|
27
|
-
# Allow extra fields even if it is not defined. This will allow models
|
|
28
|
-
# to be more flexible if features are added in the Anvil API, but
|
|
29
|
-
# explicit support hasn't been added yet to this library.
|
|
30
|
-
extra = Extra.allow
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|