python-anvil 5.0.3b1__tar.gz → 5.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/PKG-INFO +1 -1
  2. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/pyproject.toml +1 -1
  3. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api.py +5 -1
  4. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/requests.py +20 -0
  5. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/constants.py +8 -1
  6. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/test_api.py +59 -0
  7. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/LICENSE.md +0 -0
  8. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/README.md +0 -0
  9. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/__init__.py +0 -0
  10. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/__init__.py +0 -0
  11. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/base.py +0 -0
  12. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/mutations/__init__.py +0 -0
  13. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/mutations/base.py +0 -0
  14. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/mutations/create_etch_packet.py +0 -0
  15. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/mutations/forge_submit.py +0 -0
  16. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/mutations/generate_etch_signing_url.py +0 -0
  17. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/mutations/helpers.py +0 -0
  18. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/api_resources/payload.py +0 -0
  19. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/cli.py +0 -0
  20. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/exceptions.py +0 -0
  21. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/http.py +0 -0
  22. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/models.py +0 -0
  23. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/py.typed +0 -0
  24. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/__init__.py +0 -0
  25. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/conftest.py +0 -0
  26. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/payloads.py +0 -0
  27. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/test_cli.py +0 -0
  28. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/test_http.py +0 -0
  29. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/test_models.py +0 -0
  30. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/tests/test_utils.py +0 -0
  31. {python_anvil-5.0.3b1 → python_anvil-5.1.0}/python_anvil/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python_anvil
3
- Version: 5.0.3b1
3
+ Version: 5.1.0
4
4
  Summary: Anvil API
5
5
  License: MIT
6
6
  Keywords: anvil,api,pdf,signing
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
 
3
3
  name = "python_anvil"
4
- version = "5.0.3b1"
4
+ version = "5.1.0"
5
5
  description = "Anvil API"
6
6
  license = "MIT"
7
7
  authors = ["Anvil Foundry Inc. <developers@useanvil.com>"]
@@ -15,7 +15,7 @@ from .api_resources.payload import (
15
15
  ForgeSubmitPayload,
16
16
  GeneratePDFPayload,
17
17
  )
18
- from .api_resources.requests import PlainRequest, RestRequest
18
+ from .api_resources.requests import FullyQualifiedRequest, PlainRequest, RestRequest
19
19
  from .http import GQLClient, HTTPClient
20
20
 
21
21
 
@@ -117,6 +117,10 @@ class Anvil:
117
117
  api = RestRequest(self.client, options=options)
118
118
  return api
119
119
 
120
+ def request_fully_qualified(self, options: Optional[dict] = None):
121
+ api = FullyQualifiedRequest(self.client, options=options)
122
+ return api
123
+
120
124
  def fill_pdf(
121
125
  self, template_id: str, payload: Union[dict, AnyStr, FillPDFPayload], **kwargs
122
126
  ):
@@ -1,5 +1,6 @@
1
1
  from typing import Any, Dict
2
2
 
3
+ from python_anvil.constants import VALID_HOSTS
3
4
  from python_anvil.http import HTTPClient
4
5
 
5
6
 
@@ -161,3 +162,22 @@ class PlainRequest(BaseAnvilHttpRequest):
161
162
 
162
163
  def get_url(self):
163
164
  return f"{self.API_HOST}/{self.API_BASE}"
165
+
166
+
167
+ class FullyQualifiedRequest(BaseAnvilHttpRequest):
168
+ """A request class that validates URLs point to Anvil domains."""
169
+
170
+ def get_url(self):
171
+ return "" # Not used since we expect full URLs
172
+
173
+ def _validate_url(self, url):
174
+ if not any(url.startswith(host) for host in VALID_HOSTS):
175
+ raise ValueError(f"URL must start with one of: {', '.join(VALID_HOSTS)}")
176
+
177
+ def get(self, url, params=None, **kwargs):
178
+ self._validate_url(url)
179
+ return super().get(url, params, **kwargs)
180
+
181
+ def post(self, url, data=None, **kwargs):
182
+ self._validate_url(url)
183
+ return super().post(url, data, **kwargs)
@@ -1,7 +1,14 @@
1
1
  """Basic constants used in the library."""
2
2
 
3
3
  GRAPHQL_ENDPOINT: str = "https://graphql.useanvil.com"
4
- REST_ENDPOINT = "https://app.useanvil.com/api/v1/"
4
+ REST_ENDPOINT = "https://app.useanvil.com/api/v1"
5
+ ANVIL_HOST = "https://app.useanvil.com"
6
+
7
+ VALID_HOSTS = [
8
+ ANVIL_HOST,
9
+ REST_ENDPOINT,
10
+ GRAPHQL_ENDPOINT,
11
+ ]
5
12
 
6
13
  RETRIES_LIMIT = 5
7
14
  REQUESTS_LIMIT = {
@@ -9,6 +9,7 @@ from python_anvil.api_resources.payload import (
9
9
  CreateEtchPacketPayload,
10
10
  ForgeSubmitPayload,
11
11
  )
12
+ from python_anvil.constants import VALID_HOSTS
12
13
 
13
14
  from ..api_resources.payload import FillPDFPayload
14
15
  from . import payloads
@@ -396,3 +397,61 @@ def describe_api():
396
397
  anvil.forge_submit(payload=payload)
397
398
  assert m_request_post.call_count == 1
398
399
  assert _expected_data in m_request_post.call_args
400
+
401
+ def describe_rest_request_absolute_url_behavior():
402
+ @pytest.mark.parametrize(
403
+ "url, should_raise",
404
+ [
405
+ ("some/relative/path", True),
406
+ ("https://external.example.com/full/path/file.pdf", True),
407
+ *[(host + "/some-endpoint", False) for host in VALID_HOSTS],
408
+ ],
409
+ )
410
+ @mock.patch("python_anvil.api_resources.requests.AnvilRequest._request")
411
+ def test_get_behavior(mock_request, anvil, url, should_raise):
412
+ mock_request.return_value = (b"fake_content", 200, {})
413
+ rest_client = anvil.request_fully_qualified()
414
+
415
+ if should_raise:
416
+ with pytest.raises(
417
+ ValueError,
418
+ match="URL must start with one of: https://app.useanvil.com",
419
+ ):
420
+ rest_client.get(url)
421
+ else:
422
+ rest_client.get(url)
423
+ mock_request.assert_called_once_with(
424
+ "GET",
425
+ url,
426
+ params=None,
427
+ retry=True,
428
+ )
429
+
430
+ @pytest.mark.parametrize(
431
+ "url, should_raise",
432
+ [
433
+ ("some/relative/path", True),
434
+ ("https://external.example.com/full/path/file.pdf", True),
435
+ *[(host + "/some-endpoint", False) for host in VALID_HOSTS],
436
+ ],
437
+ )
438
+ @mock.patch("python_anvil.api_resources.requests.AnvilRequest._request")
439
+ def test_post_behavior(mock_request, anvil, url, should_raise):
440
+ mock_request.return_value = (b"fake_content", 200, {})
441
+ rest_client = anvil.request_fully_qualified()
442
+
443
+ if should_raise:
444
+ with pytest.raises(
445
+ ValueError,
446
+ match="URL must start with one of: https://app.useanvil.com",
447
+ ):
448
+ rest_client.post(url, data={})
449
+ else:
450
+ rest_client.post(url, data={})
451
+ mock_request.assert_called_once_with(
452
+ "POST",
453
+ url,
454
+ json={},
455
+ retry=True,
456
+ params=None,
457
+ )
File without changes
File without changes