anis-partners 1.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.
- anis_partners-1.0.0/.gitignore +12 -0
- anis_partners-1.0.0/CHANGELOG.md +5 -0
- anis_partners-1.0.0/LICENSE +21 -0
- anis_partners-1.0.0/PKG-INFO +126 -0
- anis_partners-1.0.0/README.md +96 -0
- anis_partners-1.0.0/contracts/error-catalogue.json +271 -0
- anis_partners-1.0.0/contracts/partner-public-v1.json +177 -0
- anis_partners-1.0.0/docs/caching.md +46 -0
- anis_partners-1.0.0/docs/errors.md +88 -0
- anis_partners-1.0.0/docs/getting-started.md +145 -0
- anis_partners-1.0.0/docs/observability.md +35 -0
- anis_partners-1.0.0/docs/orders-and-recovery.md +92 -0
- anis_partners-1.0.0/docs/routes-and-permissions.md +31 -0
- anis_partners-1.0.0/docs/security.md +36 -0
- anis_partners-1.0.0/pyproject.toml +74 -0
- anis_partners-1.0.0/samples/__init__.py +1 -0
- anis_partners-1.0.0/samples/console/README.md +32 -0
- anis_partners-1.0.0/samples/console/__init__.py +1 -0
- anis_partners-1.0.0/samples/console/anis_sample.py +532 -0
- anis_partners-1.0.0/src/anis_partners/__init__.py +201 -0
- anis_partners-1.0.0/src/anis_partners/_internal/__init__.py +1 -0
- anis_partners-1.0.0/src/anis_partners/_internal/base64url.py +24 -0
- anis_partners-1.0.0/src/anis_partners/_internal/clock.py +21 -0
- anis_partners-1.0.0/src/anis_partners/_internal/http_headers.py +6 -0
- anis_partners-1.0.0/src/anis_partners/_internal/key_document_cache.py +162 -0
- anis_partners-1.0.0/src/anis_partners/_internal/telemetry.py +87 -0
- anis_partners-1.0.0/src/anis_partners/_internal/uuid.py +23 -0
- anis_partners-1.0.0/src/anis_partners/client.py +144 -0
- anis_partners-1.0.0/src/anis_partners/enrollment/__init__.py +16 -0
- anis_partners-1.0.0/src/anis_partners/enrollment/client.py +284 -0
- anis_partners-1.0.0/src/anis_partners/enrollment/enrollment_proof.py +72 -0
- anis_partners-1.0.0/src/anis_partners/enrollment/key_thumbprint.py +23 -0
- anis_partners-1.0.0/src/anis_partners/enrollment/safety_code.py +31 -0
- anis_partners-1.0.0/src/anis_partners/errors/__init__.py +56 -0
- anis_partners-1.0.0/src/anis_partners/errors/_codes.py +345 -0
- anis_partners-1.0.0/src/anis_partners/errors/anis_api_error.py +207 -0
- anis_partners-1.0.0/src/anis_partners/errors/base.py +20 -0
- anis_partners-1.0.0/src/anis_partners/models/__init__.py +81 -0
- anis_partners-1.0.0/src/anis_partners/models/_json.py +175 -0
- anis_partners-1.0.0/src/anis_partners/models/cards.py +172 -0
- anis_partners-1.0.0/src/anis_partners/models/catalogue.py +191 -0
- anis_partners-1.0.0/src/anis_partners/models/enrollment.py +210 -0
- anis_partners-1.0.0/src/anis_partners/models/money.py +102 -0
- anis_partners-1.0.0/src/anis_partners/models/orders.py +182 -0
- anis_partners-1.0.0/src/anis_partners/models/problem.py +37 -0
- anis_partners-1.0.0/src/anis_partners/models/wallets.py +121 -0
- anis_partners-1.0.0/src/anis_partners/observability/__init__.py +5 -0
- anis_partners-1.0.0/src/anis_partners/observability/telemetry.py +88 -0
- anis_partners-1.0.0/src/anis_partners/operations/__init__.py +34 -0
- anis_partners-1.0.0/src/anis_partners/operations/groups.py +627 -0
- anis_partners-1.0.0/src/anis_partners/operations/routes.py +49 -0
- anis_partners-1.0.0/src/anis_partners/operations/transport.py +656 -0
- anis_partners-1.0.0/src/anis_partners/options.py +196 -0
- anis_partners-1.0.0/src/anis_partners/py.typed +0 -0
- anis_partners-1.0.0/src/anis_partners/signing/__init__.py +19 -0
- anis_partners-1.0.0/src/anis_partners/signing/content_digest.py +14 -0
- anis_partners-1.0.0/src/anis_partners/signing/ecdsa_signature_format.py +21 -0
- anis_partners-1.0.0/src/anis_partners/signing/errors.py +7 -0
- anis_partners-1.0.0/src/anis_partners/signing/keyed_signer.py +24 -0
- anis_partners-1.0.0/src/anis_partners/signing/nonce.py +21 -0
- anis_partners-1.0.0/src/anis_partners/signing/p256_signer.py +36 -0
- anis_partners-1.0.0/src/anis_partners/signing/partner_request_signature_base.py +35 -0
- anis_partners-1.0.0/src/anis_partners/signing/partner_request_signer.py +99 -0
- anis_partners-1.0.0/src/anis_partners/signing/pem_p256_signer.py +58 -0
- anis_partners-1.0.0/src/anis_partners/signing/signature_inputs.py +66 -0
- anis_partners-1.0.0/src/anis_partners/signing/signature_profile.py +34 -0
- anis_partners-1.0.0/src/anis_partners/signing/signed_request_headers.py +32 -0
- anis_partners-1.0.0/src/anis_partners/verification/__init__.py +21 -0
- anis_partners-1.0.0/src/anis_partners/verification/async_http_signing_key_source.py +143 -0
- anis_partners-1.0.0/src/anis_partners/verification/errors.py +17 -0
- anis_partners-1.0.0/src/anis_partners/verification/failures.py +28 -0
- anis_partners-1.0.0/src/anis_partners/verification/http_signing_key_source.py +139 -0
- anis_partners-1.0.0/src/anis_partners/verification/partner_jwk.py +86 -0
- anis_partners-1.0.0/src/anis_partners/verification/partner_response_signature_base.py +62 -0
- anis_partners-1.0.0/src/anis_partners/verification/partner_response_verifier.py +215 -0
- anis_partners-1.0.0/src/anis_partners/verification/signature_input_parser.py +91 -0
- anis_partners-1.0.0/src/anis_partners/verification/signing_key_source.py +25 -0
- anis_partners-1.0.0/src/anis_partners/verification/verifiable_response.py +18 -0
- anis_partners-1.0.0/tests/PARITY.md +43 -0
- anis_partners-1.0.0/tests/__init__.py +1 -0
- anis_partners-1.0.0/tests/configuration/__init__.py +1 -0
- anis_partners-1.0.0/tests/configuration/test_options.py +154 -0
- anis_partners-1.0.0/tests/conformance/__init__.py +1 -0
- anis_partners-1.0.0/tests/conformance/test_contract_drift.py +153 -0
- anis_partners-1.0.0/tests/conformance/test_enrollment_proof_vectors.py +53 -0
- anis_partners-1.0.0/tests/conformance/test_request_vectors.py +75 -0
- anis_partners-1.0.0/tests/conformance/test_response_vectors.py +123 -0
- anis_partners-1.0.0/tests/conformance/test_safety_code_vectors.py +52 -0
- anis_partners-1.0.0/tests/conftest.py +9 -0
- anis_partners-1.0.0/tests/docs/__init__.py +1 -0
- anis_partners-1.0.0/tests/docs/test_python_markdown_blocks.py +69 -0
- anis_partners-1.0.0/tests/enrollment/__init__.py +1 -0
- anis_partners-1.0.0/tests/enrollment/test_client.py +380 -0
- anis_partners-1.0.0/tests/errors/__init__.py +1 -0
- anis_partners-1.0.0/tests/errors/test_error_mapping.py +153 -0
- anis_partners-1.0.0/tests/errors/test_exception_roundtrip.py +128 -0
- anis_partners-1.0.0/tests/models/__init__.py +1 -0
- anis_partners-1.0.0/tests/models/test_model_fields.py +133 -0
- anis_partners-1.0.0/tests/models/test_money.py +82 -0
- anis_partners-1.0.0/tests/models/test_request_model_requirements.py +42 -0
- anis_partners-1.0.0/tests/observability/__init__.py +1 -0
- anis_partners-1.0.0/tests/observability/test_signals.py +672 -0
- anis_partners-1.0.0/tests/observability/test_unknown_order_message.py +38 -0
- anis_partners-1.0.0/tests/operations/__init__.py +1 -0
- anis_partners-1.0.0/tests/operations/test_pipeline.py +1435 -0
- anis_partners-1.0.0/tests/operations/test_signed_wire_capture.py +95 -0
- anis_partners-1.0.0/tests/sample/__init__.py +1 -0
- anis_partners-1.0.0/tests/sample/test_sample.py +462 -0
- anis_partners-1.0.0/tests/signing/__init__.py +1 -0
- anis_partners-1.0.0/tests/signing/test_base64url_canonical.py +13 -0
- anis_partners-1.0.0/tests/signing/test_partner_request_signer.py +80 -0
- anis_partners-1.0.0/tests/signing/test_pem_p256_signer.py +47 -0
- anis_partners-1.0.0/tests/signing/test_signature_format.py +43 -0
- anis_partners-1.0.0/tests/support/__init__.py +1 -0
- anis_partners-1.0.0/tests/support/partner.py +36 -0
- anis_partners-1.0.0/tests/support/sdk_wire.py +86 -0
- anis_partners-1.0.0/tests/support/vectors.py +17 -0
- anis_partners-1.0.0/tests/test_options.py +18 -0
- anis_partners-1.0.0/tests/test_redaction.py +53 -0
- anis_partners-1.0.0/tests/vectors/enrollment/EP-01-first-challenge.json +33 -0
- anis_partners-1.0.0/tests/vectors/enrollment/EP-02-reissued-challenge.json +33 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-01-safe-read-no-query.json +48 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-02-safe-read-with-cursor.json +48 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-03-authority-is-lower-cased.json +48 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-04-authority-with-non-default-port.json +48 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-05-query-preserved-verbatim.json +48 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-06-reveal-bodyless-nonce-mutation.json +50 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-07-diagnostic-bodyless-nonce-mutation.json +50 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-08-order-mutation.json +51 -0
- anis_partners-1.0.0/tests/vectors/request/RQ-09-invoice-reveal-bodyless-nonce-mutation.json +50 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M01.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M02.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M03.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M04.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M05.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M06.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M07.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M08.json +60 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M09.json +60 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M10.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M11.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M12.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M13.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M14.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M15.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M16.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M17.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M18.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-002-order-201-created-M01.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-002-order-201-created.json +63 -0
- anis_partners-1.0.0/tests/vectors/response/RS-003-order-202-processing-M01.json +64 -0
- anis_partners-1.0.0/tests/vectors/response/RS-003-order-202-processing.json +64 -0
- anis_partners-1.0.0/tests/vectors/response/RS-004-order-201-replayed-M01.json +63 -0
- anis_partners-1.0.0/tests/vectors/response/RS-004-order-201-replayed.json +64 -0
- anis_partners-1.0.0/tests/vectors/response/RS-005-reveal-200-M01.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-005-reveal-200.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-006-problem-429-rate-limited.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-007-enrollment-200-no-request-binding.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-008-order-200-recovered-M01.json +64 -0
- anis_partners-1.0.0/tests/vectors/response/RS-008-order-200-recovered.json +63 -0
- anis_partners-1.0.0/tests/vectors/response/RS-009-problem-409-replayed-refusal-M01.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-009-problem-409-replayed-refusal.json +63 -0
- anis_partners-1.0.0/tests/vectors/response/RS-010-invoice-reveal-200.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-101-signed-by-next-key.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-102-signed-by-retiring-key.json +61 -0
- anis_partners-1.0.0/tests/vectors/response/RS-103-key-document-carries-private-member.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-104-private-member-on-an-unused-key.json +62 -0
- anis_partners-1.0.0/tests/vectors/response/RS-105-published-key-coordinate-not-32-bytes.json +61 -0
- anis_partners-1.0.0/tests/vectors/safety-code/SC-01.json +185 -0
- anis_partners-1.0.0/tests/vectors/safety-code/SC-02.json +150 -0
- anis_partners-1.0.0/tests/vectors/safety-code/SC-03.json +170 -0
- anis_partners-1.0.0/tests/vectors/safety-code/SC-04.json +170 -0
- anis_partners-1.0.0/tests/vectors/safety-code/manifest.json +42 -0
- anis_partners-1.0.0/tests/verification/__init__.py +1 -0
- anis_partners-1.0.0/tests/verification/test_async_http_signing_key_source.py +409 -0
- anis_partners-1.0.0/tests/verification/test_http_signing_key_source.py +334 -0
- anis_partners-1.0.0/tests/verification/test_response_key.py +281 -0
- anis_partners-1.0.0/tools/__init__.py +1 -0
- anis_partners-1.0.0/tools/generate_errors.py +102 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
First release of `anis-partners`. Partners get signed requests, verified responses, typed models and errors, synchronous and asynchronous operation groups, enrollment support, safe order recovery outcomes, bounded signing-key caching, and tests against the published wire vectors and contracts.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aniscom for Technical Services (Anis)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: anis-partners
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The Python SDK for the Anis Partner API — signed requests, verified responses, typed operations.
|
|
5
|
+
Project-URL: Homepage, https://developers.anis.ly
|
|
6
|
+
Project-URL: Documentation, https://developers.anis.ly
|
|
7
|
+
Project-URL: Source, https://github.com/anis-ly/anis.partners-sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/anis-ly/anis.partners-sdk-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/anis-ly/anis.partners-sdk-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: "Aniscom for Technical Services (Anis)" <support@anis.ly>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: anis,http-signatures,libya,partner-api,rfc9421,sdk
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: cryptography>=42
|
|
27
|
+
Requires-Dist: httpx<1,>=0.27
|
|
28
|
+
Requires-Dist: opentelemetry-api>=1.25
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Anis Partners for Python
|
|
32
|
+
|
|
33
|
+
The Python client for the Anis Partner API. It signs every request, verifies every response, and gives each operation a typed result. The details that can cause duplicate purchases or expose unverified credentials are handled explicitly: caller-owned order ids, recovery outcomes, and response verification have no silent shortcuts.
|
|
34
|
+
|
|
35
|
+
> **Disclaimer.** This SDK is an optional helper provided free of charge under the MIT License, "as is", without
|
|
36
|
+
> warranty of any kind. Anis (Aniscom for Technical Services) accepts no responsibility or liability for its use or for
|
|
37
|
+
> any loss arising from it. You remain responsible for your own integration — recording orders before you send them,
|
|
38
|
+
> recovery, key custody and testing. The source code is public: read it to understand exactly what it does before you
|
|
39
|
+
> rely on it. You do not need an SDK — you can integrate directly with the Anis Partner API using the documentation at
|
|
40
|
+
> https://developers.anis.ly.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install anis-partners
|
|
44
|
+
# or
|
|
45
|
+
uv add anis-partners
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- [Getting started](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/getting-started.md) — enrollment, configuration, first call, sync and async use
|
|
49
|
+
- [Orders and recovery](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/orders-and-recovery.md) — read before placing an order
|
|
50
|
+
- [Routes and permissions](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/routes-and-permissions.md) — routes, permissions, bodies, and limits
|
|
51
|
+
- [Errors](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/errors.md) — stable error codes and typed exceptions
|
|
52
|
+
- [Security and key custody](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/security.md)
|
|
53
|
+
- [Observability](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/observability.md)
|
|
54
|
+
- [Signing key cache](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/caching.md) — shared cache for multi-process applications
|
|
55
|
+
- [Runnable sample](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/samples/console/README.md)
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from anis_partners import AnisPartnersClient, ClientOptions, PemP256Signer
|
|
61
|
+
|
|
62
|
+
options = ClientOptions("https://<authority Anis gave you>")
|
|
63
|
+
signer = PemP256Signer.from_pem_file("/secure/partner-key.pem").for_key("<active key id>")
|
|
64
|
+
|
|
65
|
+
with AnisPartnersClient(options, signer) as anis:
|
|
66
|
+
profile = anis.profile.get()
|
|
67
|
+
wallets = list(anis.wallets.list()) # follows every cursor page
|
|
68
|
+
print(profile.application.id if profile.application else "No application")
|
|
69
|
+
print(f"Found {len(wallets)} wallets")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The async client returns the same results. Run the whole flow inside an event loop:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
|
|
77
|
+
from anis_partners import AsyncAnisPartnersClient, ClientOptions, PemP256Signer
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def main() -> None:
|
|
81
|
+
options = ClientOptions("https://<authority Anis gave you>")
|
|
82
|
+
signer = PemP256Signer.from_pem_file("/secure/partner-key.pem").for_key("<active key id>")
|
|
83
|
+
async with AsyncAnisPartnersClient(options, signer) as anis:
|
|
84
|
+
profile = await anis.profile.get()
|
|
85
|
+
wallets = [wallet async for wallet in anis.wallets.list()]
|
|
86
|
+
print(profile.application.id if profile.application else "No application")
|
|
87
|
+
print(f"Found {len(wallets)} wallets")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
asyncio.run(main())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For order creation and recovery, persist the operation id and exact request before calling the SDK. See [Orders and recovery](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/orders-and-recovery.md) for the complete outcome rules.
|
|
95
|
+
|
|
96
|
+
## Refusals built into the design
|
|
97
|
+
|
|
98
|
+
The caller supplies the order id. A dropped answer followed by a new id could buy twice, so the SDK never invents this identity. The five order result types distinguish a completed sale, processing, a replay, a definitive refusal, and an unknown outcome. Only the first completion returns credentials; a replay never repeats them.
|
|
99
|
+
|
|
100
|
+
An unknown result means the original sale may have completed. Resume with the same id and body. A new purchase uses a new id only after a definitive `OrderNotPlaced` result and a changed intent.
|
|
101
|
+
|
|
102
|
+
Response verification cannot be disabled. If a read response cannot be verified, its body is discarded and `UnverifiableResponseError` is raised. An unverifiable order answer is `OrderOutcomeUnknown`, because the sale may already have completed.
|
|
103
|
+
|
|
104
|
+
## What is covered
|
|
105
|
+
|
|
106
|
+
The SDK covers all 19 published routes across `profile`, `wallets`, `catalogue`, `orders`, `owned_cards`, and `diagnostics`, plus the enrollment client. Public errors are generated from the error catalogue. Request, response, safety-code, and enrollment vectors are tested along with signed in-memory client flows and contract drift checks.
|
|
107
|
+
|
|
108
|
+
This package follows the published Partner SDK contract. Supported Python versions are 3.11–3.14.
|
|
109
|
+
|
|
110
|
+
### Live verification
|
|
111
|
+
|
|
112
|
+
Verified end to end against a live Anis environment (October 2026).
|
|
113
|
+
|
|
114
|
+
## Regenerating error types
|
|
115
|
+
|
|
116
|
+
`src/anis_partners/errors/_codes.py` is generated from `contracts/error-catalogue.json`. Regenerate it with:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv run python tools/generate_errors.py
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Do not edit the generated file by hand; the contract drift test checks it.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
[MIT](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/LICENSE) © 2026 Aniscom for Technical Services (Anis).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Anis Partners for Python
|
|
2
|
+
|
|
3
|
+
The Python client for the Anis Partner API. It signs every request, verifies every response, and gives each operation a typed result. The details that can cause duplicate purchases or expose unverified credentials are handled explicitly: caller-owned order ids, recovery outcomes, and response verification have no silent shortcuts.
|
|
4
|
+
|
|
5
|
+
> **Disclaimer.** This SDK is an optional helper provided free of charge under the MIT License, "as is", without
|
|
6
|
+
> warranty of any kind. Anis (Aniscom for Technical Services) accepts no responsibility or liability for its use or for
|
|
7
|
+
> any loss arising from it. You remain responsible for your own integration — recording orders before you send them,
|
|
8
|
+
> recovery, key custody and testing. The source code is public: read it to understand exactly what it does before you
|
|
9
|
+
> rely on it. You do not need an SDK — you can integrate directly with the Anis Partner API using the documentation at
|
|
10
|
+
> https://developers.anis.ly.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install anis-partners
|
|
14
|
+
# or
|
|
15
|
+
uv add anis-partners
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- [Getting started](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/getting-started.md) — enrollment, configuration, first call, sync and async use
|
|
19
|
+
- [Orders and recovery](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/orders-and-recovery.md) — read before placing an order
|
|
20
|
+
- [Routes and permissions](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/routes-and-permissions.md) — routes, permissions, bodies, and limits
|
|
21
|
+
- [Errors](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/errors.md) — stable error codes and typed exceptions
|
|
22
|
+
- [Security and key custody](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/security.md)
|
|
23
|
+
- [Observability](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/observability.md)
|
|
24
|
+
- [Signing key cache](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/caching.md) — shared cache for multi-process applications
|
|
25
|
+
- [Runnable sample](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/samples/console/README.md)
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from anis_partners import AnisPartnersClient, ClientOptions, PemP256Signer
|
|
31
|
+
|
|
32
|
+
options = ClientOptions("https://<authority Anis gave you>")
|
|
33
|
+
signer = PemP256Signer.from_pem_file("/secure/partner-key.pem").for_key("<active key id>")
|
|
34
|
+
|
|
35
|
+
with AnisPartnersClient(options, signer) as anis:
|
|
36
|
+
profile = anis.profile.get()
|
|
37
|
+
wallets = list(anis.wallets.list()) # follows every cursor page
|
|
38
|
+
print(profile.application.id if profile.application else "No application")
|
|
39
|
+
print(f"Found {len(wallets)} wallets")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The async client returns the same results. Run the whole flow inside an event loop:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
|
|
47
|
+
from anis_partners import AsyncAnisPartnersClient, ClientOptions, PemP256Signer
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def main() -> None:
|
|
51
|
+
options = ClientOptions("https://<authority Anis gave you>")
|
|
52
|
+
signer = PemP256Signer.from_pem_file("/secure/partner-key.pem").for_key("<active key id>")
|
|
53
|
+
async with AsyncAnisPartnersClient(options, signer) as anis:
|
|
54
|
+
profile = await anis.profile.get()
|
|
55
|
+
wallets = [wallet async for wallet in anis.wallets.list()]
|
|
56
|
+
print(profile.application.id if profile.application else "No application")
|
|
57
|
+
print(f"Found {len(wallets)} wallets")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For order creation and recovery, persist the operation id and exact request before calling the SDK. See [Orders and recovery](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/docs/orders-and-recovery.md) for the complete outcome rules.
|
|
65
|
+
|
|
66
|
+
## Refusals built into the design
|
|
67
|
+
|
|
68
|
+
The caller supplies the order id. A dropped answer followed by a new id could buy twice, so the SDK never invents this identity. The five order result types distinguish a completed sale, processing, a replay, a definitive refusal, and an unknown outcome. Only the first completion returns credentials; a replay never repeats them.
|
|
69
|
+
|
|
70
|
+
An unknown result means the original sale may have completed. Resume with the same id and body. A new purchase uses a new id only after a definitive `OrderNotPlaced` result and a changed intent.
|
|
71
|
+
|
|
72
|
+
Response verification cannot be disabled. If a read response cannot be verified, its body is discarded and `UnverifiableResponseError` is raised. An unverifiable order answer is `OrderOutcomeUnknown`, because the sale may already have completed.
|
|
73
|
+
|
|
74
|
+
## What is covered
|
|
75
|
+
|
|
76
|
+
The SDK covers all 19 published routes across `profile`, `wallets`, `catalogue`, `orders`, `owned_cards`, and `diagnostics`, plus the enrollment client. Public errors are generated from the error catalogue. Request, response, safety-code, and enrollment vectors are tested along with signed in-memory client flows and contract drift checks.
|
|
77
|
+
|
|
78
|
+
This package follows the published Partner SDK contract. Supported Python versions are 3.11–3.14.
|
|
79
|
+
|
|
80
|
+
### Live verification
|
|
81
|
+
|
|
82
|
+
Verified end to end against a live Anis environment (October 2026).
|
|
83
|
+
|
|
84
|
+
## Regenerating error types
|
|
85
|
+
|
|
86
|
+
`src/anis_partners/errors/_codes.py` is generated from `contracts/error-catalogue.json`. Regenerate it with:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run python tools/generate_errors.py
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Do not edit the generated file by hand; the contract drift test checks it.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
[MIT](https://github.com/anis-ly/anis.partners-sdk-python/blob/main/LICENSE) © 2026 Aniscom for Technical Services (Anis).
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Reduced copy of the Anis Partner API error catalogue: the public codes only, with their HTTP status, whether they are retryable and the retry guidance. Every code is documented at https://developers.anis.ly.",
|
|
3
|
+
"representations": [
|
|
4
|
+
{
|
|
5
|
+
"publicCode": "invalid_credentials",
|
|
6
|
+
"httpStatus": 401,
|
|
7
|
+
"retryable": false,
|
|
8
|
+
"retryGuidance": "Correct the signing credentials or profile before retrying; key and application state details are intentionally indistinguishable.",
|
|
9
|
+
"publicDocumentation": true
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"publicCode": "signature_expired",
|
|
13
|
+
"httpStatus": 401,
|
|
14
|
+
"retryable": true,
|
|
15
|
+
"retryGuidance": "Reserved: the current API never sends this code. A signature outside its validity window is answered invalid_credentials (401); sign again with current timestamps.",
|
|
16
|
+
"publicDocumentation": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"publicCode": "invalid_content_digest",
|
|
20
|
+
"httpStatus": 400,
|
|
21
|
+
"retryable": false,
|
|
22
|
+
"retryGuidance": "Reserved: the current API never sends this code. A Content-Digest that does not match the body is answered malformed_signed_request (400).",
|
|
23
|
+
"publicDocumentation": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"publicCode": "malformed_signed_request",
|
|
27
|
+
"httpStatus": 400,
|
|
28
|
+
"retryable": false,
|
|
29
|
+
"retryGuidance": "Rebuild how the request is signed: send both Signature and Signature-Input, cover the required components in the documented order, send every header the route requires, and compute Content-Digest over the exact body bytes. Repeating the same request unchanged fails the same way.",
|
|
30
|
+
"publicDocumentation": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"publicCode": "replay_detected",
|
|
34
|
+
"httpStatus": 409,
|
|
35
|
+
"retryable": true,
|
|
36
|
+
"retryGuidance": "Use a fresh nonce and signature while preserving the idempotency key for the same business intent.",
|
|
37
|
+
"publicDocumentation": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"publicCode": "source_ip_not_allowed",
|
|
41
|
+
"httpStatus": 403,
|
|
42
|
+
"retryable": false,
|
|
43
|
+
"retryGuidance": "Anis network protection blocked this source address. Contact Anis support with the address if it should be allowed.",
|
|
44
|
+
"publicDocumentation": true
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"publicCode": "insufficient_scope",
|
|
48
|
+
"httpStatus": 403,
|
|
49
|
+
"retryable": false,
|
|
50
|
+
"retryGuidance": "The application is not permitted to make this call: check that it holds the permission the call needs and that you call from a network agreed with Anis, then ask Anis staff to change either.",
|
|
51
|
+
"publicDocumentation": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"publicCode": "binding_not_authorized",
|
|
55
|
+
"httpStatus": 403,
|
|
56
|
+
"retryable": false,
|
|
57
|
+
"retryGuidance": "Restore the directly owned active account and Business subscription before making a new request.",
|
|
58
|
+
"publicDocumentation": true
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"publicCode": "resource_not_found",
|
|
62
|
+
"httpStatus": 404,
|
|
63
|
+
"retryable": false,
|
|
64
|
+
"retryGuidance": "Verify the resource identifier and application ownership; inaccessible resources are intentionally indistinguishable from absence.",
|
|
65
|
+
"publicDocumentation": true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"publicCode": "card_not_found",
|
|
69
|
+
"httpStatus": 404,
|
|
70
|
+
"retryable": false,
|
|
71
|
+
"retryGuidance": "Verify the sold-card identifier and wallet; absent and inaccessible single cards return the same representation.",
|
|
72
|
+
"publicDocumentation": true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"publicCode": "wallet_not_granted",
|
|
76
|
+
"httpStatus": 404,
|
|
77
|
+
"retryable": false,
|
|
78
|
+
"retryGuidance": "Obtain an approved current wallet grant before making a new request.",
|
|
79
|
+
"publicDocumentation": true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"publicCode": "validation_failed",
|
|
83
|
+
"httpStatus": 422,
|
|
84
|
+
"retryable": false,
|
|
85
|
+
"retryGuidance": "The response never names the field: check the request against the documented rules before sending it again. How the request is sent counts too: a body over 64 KB, a chunked body (send Content-Length instead), headers over 32 KB in total, a path and query over 2,048 characters, a body on a call that takes none, or an order whose Idempotency-Key is missing or is not a UUID written with hyphens.",
|
|
86
|
+
"publicDocumentation": true
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"publicCode": "idempotency_conflict",
|
|
90
|
+
"httpStatus": 409,
|
|
91
|
+
"retryable": false,
|
|
92
|
+
"retryGuidance": "Reuse the key only for identical intent; use a new UUID only for a genuinely new operation.",
|
|
93
|
+
"publicDocumentation": true
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"publicCode": "operation_processing",
|
|
97
|
+
"httpStatus": 202,
|
|
98
|
+
"retryable": true,
|
|
99
|
+
"retryGuidance": "Not an error: the order was accepted and is still being processed. Wait for the Retry-After delay, then repeat the identical POST with the same Idempotency-Key.",
|
|
100
|
+
"publicDocumentation": true
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"publicCode": "allowed_debt_consent_required",
|
|
104
|
+
"httpStatus": 402,
|
|
105
|
+
"retryable": false,
|
|
106
|
+
"retryGuidance": "Submit a new operation with explicit allowed-debt consent if the Partner intends to use debt.",
|
|
107
|
+
"publicDocumentation": true
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"publicCode": "insufficient_balance",
|
|
111
|
+
"httpStatus": 409,
|
|
112
|
+
"retryable": false,
|
|
113
|
+
"retryGuidance": "Submit a new operation only after the authoritative balance or allowed-debt state changes.",
|
|
114
|
+
"publicDocumentation": true
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"publicCode": "purchase_not_allowed",
|
|
118
|
+
"httpStatus": 403,
|
|
119
|
+
"retryable": false,
|
|
120
|
+
"retryGuidance": "The owner policy forbids this purchase; obtain authorization before submitting a new operation.",
|
|
121
|
+
"publicDocumentation": true
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"publicCode": "purchase_not_allowed",
|
|
125
|
+
"httpStatus": 409,
|
|
126
|
+
"retryable": false,
|
|
127
|
+
"retryGuidance": "The request conflicts with current owner business state; submit a new operation only after that state changes.",
|
|
128
|
+
"publicDocumentation": true
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"publicCode": "owner_limit_exceeded",
|
|
132
|
+
"httpStatus": 409,
|
|
133
|
+
"retryable": false,
|
|
134
|
+
"retryGuidance": "Do not poll; submit a new operation and idempotency key only after the owner allowance is known to have changed.",
|
|
135
|
+
"publicDocumentation": true
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"publicCode": "daily_limit_exceeded",
|
|
139
|
+
"httpStatus": 429,
|
|
140
|
+
"retryable": false,
|
|
141
|
+
"retryGuidance": "No reset time is available; submit a new operation and idempotency key only after the limit condition is known to have cleared.",
|
|
142
|
+
"publicDocumentation": true
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"publicCode": "rate_limited",
|
|
146
|
+
"httpStatus": 429,
|
|
147
|
+
"retryable": true,
|
|
148
|
+
"retryGuidance": "honour Retry-After when present; otherwise use bounded exponential backoff.",
|
|
149
|
+
"publicDocumentation": true
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"publicCode": "wallet_disabled",
|
|
153
|
+
"httpStatus": 409,
|
|
154
|
+
"retryable": false,
|
|
155
|
+
"retryGuidance": "Submit a new request only after the wallet has been re-enabled.",
|
|
156
|
+
"publicDocumentation": true
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"publicCode": "wallet_expired",
|
|
160
|
+
"httpStatus": 409,
|
|
161
|
+
"retryable": false,
|
|
162
|
+
"retryGuidance": "Submit a new request only after the account or Business subscription expiry is resolved.",
|
|
163
|
+
"publicDocumentation": true
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"publicCode": "business_subscription_required",
|
|
167
|
+
"httpStatus": 409,
|
|
168
|
+
"retryable": false,
|
|
169
|
+
"retryGuidance": "Restore an active Business subscription before making a new request.",
|
|
170
|
+
"publicDocumentation": true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"publicCode": "account_inactive",
|
|
174
|
+
"httpStatus": 403,
|
|
175
|
+
"retryable": false,
|
|
176
|
+
"retryGuidance": "Confirm or re-enable the owner account before making a new request.",
|
|
177
|
+
"publicDocumentation": true
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"publicCode": "currency_not_supported",
|
|
181
|
+
"httpStatus": 422,
|
|
182
|
+
"retryable": false,
|
|
183
|
+
"retryGuidance": "Use the currency required by the authoritative wallet and Catalogue contract.",
|
|
184
|
+
"publicDocumentation": true
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"publicCode": "card_unavailable",
|
|
188
|
+
"httpStatus": 409,
|
|
189
|
+
"retryable": false,
|
|
190
|
+
"retryGuidance": "Refresh Catalogue data and submit a new operation if the item becomes available.",
|
|
191
|
+
"publicDocumentation": true
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"publicCode": "quantity_unavailable",
|
|
195
|
+
"httpStatus": 409,
|
|
196
|
+
"retryable": false,
|
|
197
|
+
"retryGuidance": "Refresh Catalogue data or submit a new operation with an available quantity.",
|
|
198
|
+
"publicDocumentation": true
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"publicCode": "price_changed",
|
|
202
|
+
"httpStatus": 409,
|
|
203
|
+
"retryable": false,
|
|
204
|
+
"retryGuidance": "Refresh the current price and explicitly accept it in a new operation.",
|
|
205
|
+
"publicDocumentation": true
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"publicCode": "reveal_not_allowed",
|
|
209
|
+
"httpStatus": 409,
|
|
210
|
+
"retryable": false,
|
|
211
|
+
"retryGuidance": "The card-level reveal predicate refused disclosure; do not retry until the card state is known to have changed.",
|
|
212
|
+
"publicDocumentation": true
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"publicCode": "invoice_reveal_limit_exceeded",
|
|
216
|
+
"httpStatus": 409,
|
|
217
|
+
"retryable": false,
|
|
218
|
+
"retryGuidance": "Use individual reveal for selected cards; invoice reveal is capped at 100.",
|
|
219
|
+
"publicDocumentation": true
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"publicCode": "invitation_invalid",
|
|
223
|
+
"httpStatus": 401,
|
|
224
|
+
"retryable": false,
|
|
225
|
+
"retryGuidance": "Obtain a new enrollment invitation through the approved staff process.",
|
|
226
|
+
"publicDocumentation": true
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"publicCode": "challenge_expired",
|
|
230
|
+
"httpStatus": 409,
|
|
231
|
+
"retryable": false,
|
|
232
|
+
"retryGuidance": "Restart enrollment through the approved staff action and use the new challenge generation.",
|
|
233
|
+
"publicDocumentation": true
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"publicCode": "key_proof_invalid",
|
|
237
|
+
"httpStatus": 422,
|
|
238
|
+
"retryable": false,
|
|
239
|
+
"retryGuidance": "Correct the P-256/P1363 proof for the active challenge generation.",
|
|
240
|
+
"publicDocumentation": true
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"publicCode": "key_duplicate",
|
|
244
|
+
"httpStatus": 409,
|
|
245
|
+
"retryable": false,
|
|
246
|
+
"retryGuidance": "This invitation already took a key, so a newly generated key pair is refused too. Read the enrolment status first; to enrol a different key, ask Anis staff to restart the enrolment.",
|
|
247
|
+
"publicDocumentation": true
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"publicCode": "dependency_unavailable",
|
|
251
|
+
"httpStatus": 503,
|
|
252
|
+
"retryable": true,
|
|
253
|
+
"retryGuidance": "Retry with the same idempotency key for the same mutation intent after bounded backoff.",
|
|
254
|
+
"publicDocumentation": true
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"publicCode": "request_timeout",
|
|
258
|
+
"httpStatus": 504,
|
|
259
|
+
"retryable": true,
|
|
260
|
+
"retryGuidance": "Inspect operation status or repeat the identical request with the same idempotency key.",
|
|
261
|
+
"publicDocumentation": true
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"publicCode": "internal_error",
|
|
265
|
+
"httpStatus": 500,
|
|
266
|
+
"retryable": true,
|
|
267
|
+
"retryGuidance": "Preserve the same accepted operation and idempotency key; retry only after bounded backoff or platform remediation.",
|
|
268
|
+
"publicDocumentation": true
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
}
|