updo-sdk 0.6.1__tar.gz → 0.6.2__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.
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/CHANGELOG.md +18 -0
- {updo_sdk-0.6.1/src/updo_sdk.egg-info → updo_sdk-0.6.2}/PKG-INFO +3 -1
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/README.md +2 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/__init__.py +2 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/_version.py +1 -1
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/errors.py +13 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2/src/updo_sdk.egg-info}/PKG-INFO +3 -1
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo_sdk.egg-info/SOURCES.txt +1 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/data/openapi.json +4 -4
- updo_sdk-0.6.2/tests/test_cascade_delete_refused.py +69 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/LICENSE +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/MANIFEST.in +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/docs/cli.md +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/examples/quickstart.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/pyproject.toml +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/setup.cfg +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/_transport.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/aio.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/cli.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/client.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/codegen.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/config.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/models.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/py.typed +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/query.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/__init__.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/_params.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/base.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/entities.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/images.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/payments.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/records.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/resources/webhooks.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo/webhooks.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo_sdk.egg-info/dependency_links.txt +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo_sdk.egg-info/entry_points.txt +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo_sdk.egg-info/requires.txt +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/src/updo_sdk.egg-info/top_level.txt +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/__init__.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/conftest.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/integration/__init__.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/integration/test_live.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_approval_refusals.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_async.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_cli.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_client.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_codegen.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_contract.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_documents.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_downloads.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_entity_not_creatable.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_errors.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_images.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_list_fields.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_long_retry_after.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_query.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_schema_hints.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_site_refusal.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_wall_clock.py +0 -0
- {updo_sdk-0.6.1 → updo_sdk-0.6.2}/tests/test_webhooks.py +0 -0
|
@@ -4,6 +4,24 @@ Format: [Keep a Changelog](https://keepachangelog.com/1.1.0/).
|
|
|
4
4
|
Versioning: [SemVer](https://semver.org/) — on the SDK's own surface, not on the
|
|
5
5
|
platform's. A backwards-compatible server change never forces a major bump here.
|
|
6
6
|
|
|
7
|
+
## [0.6.2] — 2026-10-18
|
|
8
|
+
|
|
9
|
+
Follows Qlaris delivery 14 (which carries the end of delivery 13), checked
|
|
10
|
+
against the OpenAPI document production serves on 2026-10-18: no new route and
|
|
11
|
+
no new shape, one new refusal code. The contract fixture is refreshed to it.
|
|
12
|
+
|
|
13
|
+
### A delete refused for what it would reach
|
|
14
|
+
|
|
15
|
+
- `CascadeDeleteRefused` -- 403 `CASCADE_DELETE_REFUSED`: deleting the record
|
|
16
|
+
would remove (cascade) or rewrite (set to null) dependent records this token
|
|
17
|
+
may not delete or change, or only through an approval. The WHOLE delete is
|
|
18
|
+
refused, before anything is written. Not a refusal of the record itself:
|
|
19
|
+
deal with those records first, or ask someone who may; `exc.detail` says
|
|
20
|
+
which. A `PermissionDenied`, promoted only on a 403 with the code, so
|
|
21
|
+
`except PermissionDenied` keeps catching it. Sync and async.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
7
25
|
## [0.6.1] — 2026-10-18
|
|
8
26
|
|
|
9
27
|
Fix only.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: updo-sdk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Python SDK for the Updo360 (Qlaris) public ERP API
|
|
5
5
|
Author: UPDO Technologies Inc.
|
|
6
6
|
License-Expression: MIT
|
|
@@ -815,6 +815,7 @@ from updo import (
|
|
|
815
815
|
ApprovalNotPossible,
|
|
816
816
|
SiteNotAllowed,
|
|
817
817
|
EntityNotCreatable,
|
|
818
|
+
CascadeDeleteRefused,
|
|
818
819
|
PlanLimitExceeded,
|
|
819
820
|
DownloadInterrupted,
|
|
820
821
|
RateLimitError,
|
|
@@ -841,6 +842,7 @@ The server speaks **two error dialects** (the platform envelope
|
|
|
841
842
|
| 402 | `PlanLimitExceeded` | subscription quota reached |
|
|
842
843
|
| 403 | `PermissionDenied` | ABAC refusal, token scope ceiling, module disabled; `exc.resources` / `exc.feature` on a PDF; `exc.code == "IP_NOT_ALLOWED"` when the source IP is outside **the organisation's** security allowlist |
|
|
843
844
|
| 403 | `EntityNotCreatable` | the entity's records are made by the platform (e.g. a public holiday entitlement): nobody creates one, whatever their rights — `exc.entity` names it |
|
|
845
|
+
| 403 | `CascadeDeleteRefused` | a delete would remove or rewrite dependent records this token may not delete or change (or only through an approval): nothing is deleted — deal with those records first; `exc.detail` says which |
|
|
844
846
|
| 403 | `SiteNotAllowed` | `X-Site-ID` names a site this token may not use — unknown, inactive, foreign or not assigned (see *Multi-site*) |
|
|
845
847
|
| 403 | `ApprovalNotPossible` | the action needs an approval this request cannot wait for — `ServiceAccountCannotAwaitApproval` or `ApprovalRequiredElsewhere` (see *Writes subject to approval*) |
|
|
846
848
|
| 404 | `NotFoundError` | entity or record does not exist |
|
|
@@ -779,6 +779,7 @@ from updo import (
|
|
|
779
779
|
ApprovalNotPossible,
|
|
780
780
|
SiteNotAllowed,
|
|
781
781
|
EntityNotCreatable,
|
|
782
|
+
CascadeDeleteRefused,
|
|
782
783
|
PlanLimitExceeded,
|
|
783
784
|
DownloadInterrupted,
|
|
784
785
|
RateLimitError,
|
|
@@ -805,6 +806,7 @@ The server speaks **two error dialects** (the platform envelope
|
|
|
805
806
|
| 402 | `PlanLimitExceeded` | subscription quota reached |
|
|
806
807
|
| 403 | `PermissionDenied` | ABAC refusal, token scope ceiling, module disabled; `exc.resources` / `exc.feature` on a PDF; `exc.code == "IP_NOT_ALLOWED"` when the source IP is outside **the organisation's** security allowlist |
|
|
807
808
|
| 403 | `EntityNotCreatable` | the entity's records are made by the platform (e.g. a public holiday entitlement): nobody creates one, whatever their rights — `exc.entity` names it |
|
|
809
|
+
| 403 | `CascadeDeleteRefused` | a delete would remove or rewrite dependent records this token may not delete or change (or only through an approval): nothing is deleted — deal with those records first; `exc.detail` says which |
|
|
808
810
|
| 403 | `SiteNotAllowed` | `X-Site-ID` names a site this token may not use — unknown, inactive, foreign or not assigned (see *Multi-site*) |
|
|
809
811
|
| 403 | `ApprovalNotPossible` | the action needs an approval this request cannot wait for — `ServiceAccountCannotAwaitApproval` or `ApprovalRequiredElsewhere` (see *Writes subject to approval*) |
|
|
810
812
|
| 404 | `NotFoundError` | entity or record does not exist |
|
|
@@ -21,6 +21,7 @@ from .errors import (
|
|
|
21
21
|
ApprovalRequired,
|
|
22
22
|
ApprovalRequiredElsewhere,
|
|
23
23
|
AuthenticationError,
|
|
24
|
+
CascadeDeleteRefused,
|
|
24
25
|
ConflictError,
|
|
25
26
|
DownloadInterrupted,
|
|
26
27
|
EntityNotCreatable,
|
|
@@ -75,6 +76,7 @@ __all__ = [
|
|
|
75
76
|
"ApprovalRequired",
|
|
76
77
|
"AsyncUpdoClient",
|
|
77
78
|
"AuthenticationError",
|
|
79
|
+
"CascadeDeleteRefused",
|
|
78
80
|
"ClientConfig",
|
|
79
81
|
"ConflictError",
|
|
80
82
|
"DocumentTemplate",
|
|
@@ -23,6 +23,7 @@ from typing import Any
|
|
|
23
23
|
import httpx
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
|
+
"CascadeDeleteRefused",
|
|
26
27
|
"EntityNotCreatable",
|
|
27
28
|
"ListFieldEquality",
|
|
28
29
|
"SiteNotAllowed",
|
|
@@ -267,6 +268,17 @@ class EntityNotCreatable(PermissionDenied):
|
|
|
267
268
|
return None if value is None else str(value)
|
|
268
269
|
|
|
269
270
|
|
|
271
|
+
class CascadeDeleteRefused(PermissionDenied):
|
|
272
|
+
"""403 ``CASCADE_DELETE_REFUSED`` -- the delete would reach records this token may not touch.
|
|
273
|
+
|
|
274
|
+
Deleting the record would remove (cascade) or rewrite (set to null) other
|
|
275
|
+
records that the caller may not delete or change, or only through an
|
|
276
|
+
approval. The WHOLE delete is refused before anything is written. Not a
|
|
277
|
+
refusal of the record itself: handle the dependent records first, each
|
|
278
|
+
through its own door, or ask someone who may. ``exc.detail`` says which.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
|
|
270
282
|
class SiteNotAllowed(PermissionDenied):
|
|
271
283
|
"""403 ``SITE_NOT_ALLOWED`` -- the ``X-Site-ID`` sent is not one this token may use.
|
|
272
284
|
|
|
@@ -579,6 +591,7 @@ _CODED_403: dict[str, type[PermissionDenied]] = {
|
|
|
579
591
|
**_APPROVAL_CODES,
|
|
580
592
|
"SITE_NOT_ALLOWED": SiteNotAllowed,
|
|
581
593
|
"ENTITY_NOT_CREATABLE": EntityNotCreatable,
|
|
594
|
+
"CASCADE_DELETE_REFUSED": CascadeDeleteRefused,
|
|
582
595
|
}
|
|
583
596
|
|
|
584
597
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: updo-sdk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Python SDK for the Updo360 (Qlaris) public ERP API
|
|
5
5
|
Author: UPDO Technologies Inc.
|
|
6
6
|
License-Expression: MIT
|
|
@@ -815,6 +815,7 @@ from updo import (
|
|
|
815
815
|
ApprovalNotPossible,
|
|
816
816
|
SiteNotAllowed,
|
|
817
817
|
EntityNotCreatable,
|
|
818
|
+
CascadeDeleteRefused,
|
|
818
819
|
PlanLimitExceeded,
|
|
819
820
|
DownloadInterrupted,
|
|
820
821
|
RateLimitError,
|
|
@@ -841,6 +842,7 @@ The server speaks **two error dialects** (the platform envelope
|
|
|
841
842
|
| 402 | `PlanLimitExceeded` | subscription quota reached |
|
|
842
843
|
| 403 | `PermissionDenied` | ABAC refusal, token scope ceiling, module disabled; `exc.resources` / `exc.feature` on a PDF; `exc.code == "IP_NOT_ALLOWED"` when the source IP is outside **the organisation's** security allowlist |
|
|
843
844
|
| 403 | `EntityNotCreatable` | the entity's records are made by the platform (e.g. a public holiday entitlement): nobody creates one, whatever their rights — `exc.entity` names it |
|
|
845
|
+
| 403 | `CascadeDeleteRefused` | a delete would remove or rewrite dependent records this token may not delete or change (or only through an approval): nothing is deleted — deal with those records first; `exc.detail` says which |
|
|
844
846
|
| 403 | `SiteNotAllowed` | `X-Site-ID` names a site this token may not use — unknown, inactive, foreign or not assigned (see *Multi-site*) |
|
|
845
847
|
| 403 | `ApprovalNotPossible` | the action needs an approval this request cannot wait for — `ServiceAccountCannotAwaitApproval` or `ApprovalRequiredElsewhere` (see *Writes subject to approval*) |
|
|
846
848
|
| 404 | `NotFoundError` | entity or record does not exist |
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
"description": "A refusal of the public API.",
|
|
322
322
|
"properties": {
|
|
323
323
|
"code": {
|
|
324
|
-
"description": "Stable machine code. Known codes: `ACCESS_DENIED`, `APPROVAL_REQUIRED_ELSEWHERE`, `SERVICE_ACCOUNT_CANNOT_AWAIT_APPROVAL`, `SITE_NOT_ALLOWED`, `IP_NOT_ALLOWED`, `ENTITY_NOT_CREATABLE`, `TENANT_MEMBERSHIP_REQUIRED`, `FEATURE_NOT_LICENSED`, `FEATURE_NOT_APPLICABLE`",
|
|
324
|
+
"description": "Stable machine code. Known codes: `ACCESS_DENIED`, `APPROVAL_REQUIRED_ELSEWHERE`, `CASCADE_DELETE_REFUSED`, `SERVICE_ACCOUNT_CANNOT_AWAIT_APPROVAL`, `SITE_NOT_ALLOWED`, `IP_NOT_ALLOWED`, `ENTITY_NOT_CREATABLE`, `TENANT_MEMBERSHIP_REQUIRED`, `FEATURE_NOT_LICENSED`, `FEATURE_NOT_APPLICABLE`",
|
|
325
325
|
"type": "string"
|
|
326
326
|
},
|
|
327
327
|
"detail": {
|
|
@@ -1416,7 +1416,7 @@
|
|
|
1416
1416
|
}
|
|
1417
1417
|
}
|
|
1418
1418
|
},
|
|
1419
|
-
"description": "Refused. `code` is one of: `ACCESS_DENIED`, `SERVICE_ACCOUNT_CANNOT_AWAIT_APPROVAL`, `SITE_NOT_ALLOWED`, `IP_NOT_ALLOWED`."
|
|
1419
|
+
"description": "Refused. `code` is one of: `ACCESS_DENIED`, `SERVICE_ACCOUNT_CANNOT_AWAIT_APPROVAL`, `CASCADE_DELETE_REFUSED`, `SITE_NOT_ALLOWED`, `IP_NOT_ALLOWED`."
|
|
1420
1420
|
}
|
|
1421
1421
|
},
|
|
1422
1422
|
"security": [
|
|
@@ -2017,7 +2017,7 @@
|
|
|
2017
2017
|
},
|
|
2018
2018
|
"/api/public/v1/me/": {
|
|
2019
2019
|
"get": {
|
|
2020
|
-
"description": "`GET /api/public/v1/me/` \u2014 what THIS token is, and what it can do.\n\n\u26a0\ufe0f This endpoint is the COUNTERPART of a rule: since the public API became\nmandatory, an API token no longer authenticates anywhere else. The only\nfrontend route integrations actually targeted was `/api/v1/users/context/`\n\u2014 blocking it without an equivalent would have REMOVED a capability\ninstead of relocating it, and \"mandatory\" only makes sense if the public\nsurface is sufficient.\n\nWe only return what a MACHINE client can act on: the organization the\ntoken is bound to, its scopes, and its expiry. Nothing from the screen\ncontext \u2014 no menus, preferences or
|
|
2020
|
+
"description": "`GET /api/public/v1/me/` \u2014 what THIS token is, and what it can do.\n\n\u26a0\ufe0f This endpoint is the COUNTERPART of a rule: since the public API became\nmandatory, an API token no longer authenticates anywhere else. The only\nfrontend route integrations actually targeted was `/api/v1/users/context/`\n\u2014 blocking it without an equivalent would have REMOVED a capability\ninstead of relocating it, and \"mandatory\" only makes sense if the public\nsurface is sufficient.\n\nWe only return what a MACHINE client can act on: the organization the\ntoken is bound to, its scopes, and its expiry. Nothing from the screen\ncontext \u2014 no menus, preferences or Studio capabilities: those are\ninterface needs, and they change with the interface.",
|
|
2021
2021
|
"operationId": "me_retrieve",
|
|
2022
2022
|
"parameters": [
|
|
2023
2023
|
{
|
|
@@ -2412,7 +2412,7 @@
|
|
|
2412
2412
|
},
|
|
2413
2413
|
"/api/public/v1/webhooks/deliveries/{id}/": {
|
|
2414
2414
|
"get": {
|
|
2415
|
-
"description": "The delivery journal, read-only on the machine boundary.\n\nSame filters as the
|
|
2415
|
+
"description": "The delivery journal, read-only on the machine boundary.\n\nSame filters as the Studio surface (``?binding=`` / ``?status=`` /\n``?event_id=``); rows arrive truncated + secret-masked from write time.",
|
|
2416
2416
|
"operationId": "webhooks_deliveries_retrieve",
|
|
2417
2417
|
"parameters": [
|
|
2418
2418
|
{
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""403 CASCADE_DELETE_REFUSED -- a delete that would reach records the token may not touch.
|
|
2
|
+
|
|
3
|
+
Qlaris delivery 14 (security MR !70): the delete would remove or rewrite
|
|
4
|
+
dependent records the caller may not delete or change, or only through an
|
|
5
|
+
approval. The whole delete is refused before anything is written.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from updo import CascadeDeleteRefused
|
|
14
|
+
from updo.errors import ApprovalNotPossible, EntityNotCreatable, PermissionDenied, SiteNotAllowed
|
|
15
|
+
|
|
16
|
+
from .conftest import API_ROOT
|
|
17
|
+
|
|
18
|
+
RECORD = "0f8fad5b-d9cb-469f-a165-70867728950e"
|
|
19
|
+
URL = f"{API_ROOT}/data/customer/{RECORD}/"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _refusal(status: int = 403) -> httpx.Response:
|
|
23
|
+
return httpx.Response(
|
|
24
|
+
status,
|
|
25
|
+
json={
|
|
26
|
+
"code": "CASCADE_DELETE_REFUSED",
|
|
27
|
+
"detail": "Deleting this record would also delete 3 invoices you may not delete.",
|
|
28
|
+
"field_errors": None,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_a_delete_is_refused_with_its_own_class(respx_mock, client) -> None:
|
|
34
|
+
route = respx_mock.delete(URL).mock(return_value=_refusal())
|
|
35
|
+
with pytest.raises(CascadeDeleteRefused) as excinfo:
|
|
36
|
+
client.entity("customer").delete(RECORD)
|
|
37
|
+
error = excinfo.value
|
|
38
|
+
assert error.code == "CASCADE_DELETE_REFUSED"
|
|
39
|
+
assert "invoices" in error.detail
|
|
40
|
+
# A 403 like the others, but none of the other remedies applies.
|
|
41
|
+
assert isinstance(error, PermissionDenied)
|
|
42
|
+
assert not isinstance(error, (ApprovalNotPossible, SiteNotAllowed, EntityNotCreatable))
|
|
43
|
+
# A refusal of the server's judgment, not a transient fault: never replayed.
|
|
44
|
+
assert route.call_count == 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_the_code_under_another_status_is_not_promoted(respx_mock, client) -> None:
|
|
48
|
+
respx_mock.delete(URL).mock(return_value=_refusal(400))
|
|
49
|
+
with pytest.raises(Exception) as excinfo:
|
|
50
|
+
client.entity("customer").delete(RECORD)
|
|
51
|
+
assert not isinstance(excinfo.value, CascadeDeleteRefused)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_a_plain_access_denied_stays_a_plain_permission_denied(respx_mock, client) -> None:
|
|
55
|
+
respx_mock.delete(URL).mock(
|
|
56
|
+
return_value=httpx.Response(
|
|
57
|
+
403, json={"code": "ACCESS_DENIED", "detail": "x", "field_errors": None}
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
with pytest.raises(PermissionDenied) as excinfo:
|
|
61
|
+
client.entity("customer").delete(RECORD)
|
|
62
|
+
assert type(excinfo.value) is PermissionDenied
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def test_async_promotes_it(respx_mock, async_client) -> None:
|
|
66
|
+
respx_mock.delete(URL).mock(return_value=_refusal())
|
|
67
|
+
resource = await async_client.entity("customer")
|
|
68
|
+
with pytest.raises(CascadeDeleteRefused):
|
|
69
|
+
await resource.delete(RECORD)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|