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.
Files changed (180) hide show
  1. anis_partners-1.0.0/.gitignore +12 -0
  2. anis_partners-1.0.0/CHANGELOG.md +5 -0
  3. anis_partners-1.0.0/LICENSE +21 -0
  4. anis_partners-1.0.0/PKG-INFO +126 -0
  5. anis_partners-1.0.0/README.md +96 -0
  6. anis_partners-1.0.0/contracts/error-catalogue.json +271 -0
  7. anis_partners-1.0.0/contracts/partner-public-v1.json +177 -0
  8. anis_partners-1.0.0/docs/caching.md +46 -0
  9. anis_partners-1.0.0/docs/errors.md +88 -0
  10. anis_partners-1.0.0/docs/getting-started.md +145 -0
  11. anis_partners-1.0.0/docs/observability.md +35 -0
  12. anis_partners-1.0.0/docs/orders-and-recovery.md +92 -0
  13. anis_partners-1.0.0/docs/routes-and-permissions.md +31 -0
  14. anis_partners-1.0.0/docs/security.md +36 -0
  15. anis_partners-1.0.0/pyproject.toml +74 -0
  16. anis_partners-1.0.0/samples/__init__.py +1 -0
  17. anis_partners-1.0.0/samples/console/README.md +32 -0
  18. anis_partners-1.0.0/samples/console/__init__.py +1 -0
  19. anis_partners-1.0.0/samples/console/anis_sample.py +532 -0
  20. anis_partners-1.0.0/src/anis_partners/__init__.py +201 -0
  21. anis_partners-1.0.0/src/anis_partners/_internal/__init__.py +1 -0
  22. anis_partners-1.0.0/src/anis_partners/_internal/base64url.py +24 -0
  23. anis_partners-1.0.0/src/anis_partners/_internal/clock.py +21 -0
  24. anis_partners-1.0.0/src/anis_partners/_internal/http_headers.py +6 -0
  25. anis_partners-1.0.0/src/anis_partners/_internal/key_document_cache.py +162 -0
  26. anis_partners-1.0.0/src/anis_partners/_internal/telemetry.py +87 -0
  27. anis_partners-1.0.0/src/anis_partners/_internal/uuid.py +23 -0
  28. anis_partners-1.0.0/src/anis_partners/client.py +144 -0
  29. anis_partners-1.0.0/src/anis_partners/enrollment/__init__.py +16 -0
  30. anis_partners-1.0.0/src/anis_partners/enrollment/client.py +284 -0
  31. anis_partners-1.0.0/src/anis_partners/enrollment/enrollment_proof.py +72 -0
  32. anis_partners-1.0.0/src/anis_partners/enrollment/key_thumbprint.py +23 -0
  33. anis_partners-1.0.0/src/anis_partners/enrollment/safety_code.py +31 -0
  34. anis_partners-1.0.0/src/anis_partners/errors/__init__.py +56 -0
  35. anis_partners-1.0.0/src/anis_partners/errors/_codes.py +345 -0
  36. anis_partners-1.0.0/src/anis_partners/errors/anis_api_error.py +207 -0
  37. anis_partners-1.0.0/src/anis_partners/errors/base.py +20 -0
  38. anis_partners-1.0.0/src/anis_partners/models/__init__.py +81 -0
  39. anis_partners-1.0.0/src/anis_partners/models/_json.py +175 -0
  40. anis_partners-1.0.0/src/anis_partners/models/cards.py +172 -0
  41. anis_partners-1.0.0/src/anis_partners/models/catalogue.py +191 -0
  42. anis_partners-1.0.0/src/anis_partners/models/enrollment.py +210 -0
  43. anis_partners-1.0.0/src/anis_partners/models/money.py +102 -0
  44. anis_partners-1.0.0/src/anis_partners/models/orders.py +182 -0
  45. anis_partners-1.0.0/src/anis_partners/models/problem.py +37 -0
  46. anis_partners-1.0.0/src/anis_partners/models/wallets.py +121 -0
  47. anis_partners-1.0.0/src/anis_partners/observability/__init__.py +5 -0
  48. anis_partners-1.0.0/src/anis_partners/observability/telemetry.py +88 -0
  49. anis_partners-1.0.0/src/anis_partners/operations/__init__.py +34 -0
  50. anis_partners-1.0.0/src/anis_partners/operations/groups.py +627 -0
  51. anis_partners-1.0.0/src/anis_partners/operations/routes.py +49 -0
  52. anis_partners-1.0.0/src/anis_partners/operations/transport.py +656 -0
  53. anis_partners-1.0.0/src/anis_partners/options.py +196 -0
  54. anis_partners-1.0.0/src/anis_partners/py.typed +0 -0
  55. anis_partners-1.0.0/src/anis_partners/signing/__init__.py +19 -0
  56. anis_partners-1.0.0/src/anis_partners/signing/content_digest.py +14 -0
  57. anis_partners-1.0.0/src/anis_partners/signing/ecdsa_signature_format.py +21 -0
  58. anis_partners-1.0.0/src/anis_partners/signing/errors.py +7 -0
  59. anis_partners-1.0.0/src/anis_partners/signing/keyed_signer.py +24 -0
  60. anis_partners-1.0.0/src/anis_partners/signing/nonce.py +21 -0
  61. anis_partners-1.0.0/src/anis_partners/signing/p256_signer.py +36 -0
  62. anis_partners-1.0.0/src/anis_partners/signing/partner_request_signature_base.py +35 -0
  63. anis_partners-1.0.0/src/anis_partners/signing/partner_request_signer.py +99 -0
  64. anis_partners-1.0.0/src/anis_partners/signing/pem_p256_signer.py +58 -0
  65. anis_partners-1.0.0/src/anis_partners/signing/signature_inputs.py +66 -0
  66. anis_partners-1.0.0/src/anis_partners/signing/signature_profile.py +34 -0
  67. anis_partners-1.0.0/src/anis_partners/signing/signed_request_headers.py +32 -0
  68. anis_partners-1.0.0/src/anis_partners/verification/__init__.py +21 -0
  69. anis_partners-1.0.0/src/anis_partners/verification/async_http_signing_key_source.py +143 -0
  70. anis_partners-1.0.0/src/anis_partners/verification/errors.py +17 -0
  71. anis_partners-1.0.0/src/anis_partners/verification/failures.py +28 -0
  72. anis_partners-1.0.0/src/anis_partners/verification/http_signing_key_source.py +139 -0
  73. anis_partners-1.0.0/src/anis_partners/verification/partner_jwk.py +86 -0
  74. anis_partners-1.0.0/src/anis_partners/verification/partner_response_signature_base.py +62 -0
  75. anis_partners-1.0.0/src/anis_partners/verification/partner_response_verifier.py +215 -0
  76. anis_partners-1.0.0/src/anis_partners/verification/signature_input_parser.py +91 -0
  77. anis_partners-1.0.0/src/anis_partners/verification/signing_key_source.py +25 -0
  78. anis_partners-1.0.0/src/anis_partners/verification/verifiable_response.py +18 -0
  79. anis_partners-1.0.0/tests/PARITY.md +43 -0
  80. anis_partners-1.0.0/tests/__init__.py +1 -0
  81. anis_partners-1.0.0/tests/configuration/__init__.py +1 -0
  82. anis_partners-1.0.0/tests/configuration/test_options.py +154 -0
  83. anis_partners-1.0.0/tests/conformance/__init__.py +1 -0
  84. anis_partners-1.0.0/tests/conformance/test_contract_drift.py +153 -0
  85. anis_partners-1.0.0/tests/conformance/test_enrollment_proof_vectors.py +53 -0
  86. anis_partners-1.0.0/tests/conformance/test_request_vectors.py +75 -0
  87. anis_partners-1.0.0/tests/conformance/test_response_vectors.py +123 -0
  88. anis_partners-1.0.0/tests/conformance/test_safety_code_vectors.py +52 -0
  89. anis_partners-1.0.0/tests/conftest.py +9 -0
  90. anis_partners-1.0.0/tests/docs/__init__.py +1 -0
  91. anis_partners-1.0.0/tests/docs/test_python_markdown_blocks.py +69 -0
  92. anis_partners-1.0.0/tests/enrollment/__init__.py +1 -0
  93. anis_partners-1.0.0/tests/enrollment/test_client.py +380 -0
  94. anis_partners-1.0.0/tests/errors/__init__.py +1 -0
  95. anis_partners-1.0.0/tests/errors/test_error_mapping.py +153 -0
  96. anis_partners-1.0.0/tests/errors/test_exception_roundtrip.py +128 -0
  97. anis_partners-1.0.0/tests/models/__init__.py +1 -0
  98. anis_partners-1.0.0/tests/models/test_model_fields.py +133 -0
  99. anis_partners-1.0.0/tests/models/test_money.py +82 -0
  100. anis_partners-1.0.0/tests/models/test_request_model_requirements.py +42 -0
  101. anis_partners-1.0.0/tests/observability/__init__.py +1 -0
  102. anis_partners-1.0.0/tests/observability/test_signals.py +672 -0
  103. anis_partners-1.0.0/tests/observability/test_unknown_order_message.py +38 -0
  104. anis_partners-1.0.0/tests/operations/__init__.py +1 -0
  105. anis_partners-1.0.0/tests/operations/test_pipeline.py +1435 -0
  106. anis_partners-1.0.0/tests/operations/test_signed_wire_capture.py +95 -0
  107. anis_partners-1.0.0/tests/sample/__init__.py +1 -0
  108. anis_partners-1.0.0/tests/sample/test_sample.py +462 -0
  109. anis_partners-1.0.0/tests/signing/__init__.py +1 -0
  110. anis_partners-1.0.0/tests/signing/test_base64url_canonical.py +13 -0
  111. anis_partners-1.0.0/tests/signing/test_partner_request_signer.py +80 -0
  112. anis_partners-1.0.0/tests/signing/test_pem_p256_signer.py +47 -0
  113. anis_partners-1.0.0/tests/signing/test_signature_format.py +43 -0
  114. anis_partners-1.0.0/tests/support/__init__.py +1 -0
  115. anis_partners-1.0.0/tests/support/partner.py +36 -0
  116. anis_partners-1.0.0/tests/support/sdk_wire.py +86 -0
  117. anis_partners-1.0.0/tests/support/vectors.py +17 -0
  118. anis_partners-1.0.0/tests/test_options.py +18 -0
  119. anis_partners-1.0.0/tests/test_redaction.py +53 -0
  120. anis_partners-1.0.0/tests/vectors/enrollment/EP-01-first-challenge.json +33 -0
  121. anis_partners-1.0.0/tests/vectors/enrollment/EP-02-reissued-challenge.json +33 -0
  122. anis_partners-1.0.0/tests/vectors/request/RQ-01-safe-read-no-query.json +48 -0
  123. anis_partners-1.0.0/tests/vectors/request/RQ-02-safe-read-with-cursor.json +48 -0
  124. anis_partners-1.0.0/tests/vectors/request/RQ-03-authority-is-lower-cased.json +48 -0
  125. anis_partners-1.0.0/tests/vectors/request/RQ-04-authority-with-non-default-port.json +48 -0
  126. anis_partners-1.0.0/tests/vectors/request/RQ-05-query-preserved-verbatim.json +48 -0
  127. anis_partners-1.0.0/tests/vectors/request/RQ-06-reveal-bodyless-nonce-mutation.json +50 -0
  128. anis_partners-1.0.0/tests/vectors/request/RQ-07-diagnostic-bodyless-nonce-mutation.json +50 -0
  129. anis_partners-1.0.0/tests/vectors/request/RQ-08-order-mutation.json +51 -0
  130. anis_partners-1.0.0/tests/vectors/request/RQ-09-invoice-reveal-bodyless-nonce-mutation.json +50 -0
  131. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M01.json +61 -0
  132. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M02.json +61 -0
  133. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M03.json +61 -0
  134. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M04.json +61 -0
  135. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M05.json +61 -0
  136. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M06.json +61 -0
  137. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M07.json +61 -0
  138. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M08.json +60 -0
  139. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M09.json +60 -0
  140. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M10.json +61 -0
  141. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M11.json +61 -0
  142. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M12.json +61 -0
  143. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M13.json +61 -0
  144. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M14.json +61 -0
  145. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M15.json +61 -0
  146. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M16.json +61 -0
  147. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M17.json +61 -0
  148. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200-M18.json +62 -0
  149. anis_partners-1.0.0/tests/vectors/response/RS-001-safe-read-profile-200.json +61 -0
  150. anis_partners-1.0.0/tests/vectors/response/RS-002-order-201-created-M01.json +62 -0
  151. anis_partners-1.0.0/tests/vectors/response/RS-002-order-201-created.json +63 -0
  152. anis_partners-1.0.0/tests/vectors/response/RS-003-order-202-processing-M01.json +64 -0
  153. anis_partners-1.0.0/tests/vectors/response/RS-003-order-202-processing.json +64 -0
  154. anis_partners-1.0.0/tests/vectors/response/RS-004-order-201-replayed-M01.json +63 -0
  155. anis_partners-1.0.0/tests/vectors/response/RS-004-order-201-replayed.json +64 -0
  156. anis_partners-1.0.0/tests/vectors/response/RS-005-reveal-200-M01.json +61 -0
  157. anis_partners-1.0.0/tests/vectors/response/RS-005-reveal-200.json +62 -0
  158. anis_partners-1.0.0/tests/vectors/response/RS-006-problem-429-rate-limited.json +62 -0
  159. anis_partners-1.0.0/tests/vectors/response/RS-007-enrollment-200-no-request-binding.json +62 -0
  160. anis_partners-1.0.0/tests/vectors/response/RS-008-order-200-recovered-M01.json +64 -0
  161. anis_partners-1.0.0/tests/vectors/response/RS-008-order-200-recovered.json +63 -0
  162. anis_partners-1.0.0/tests/vectors/response/RS-009-problem-409-replayed-refusal-M01.json +62 -0
  163. anis_partners-1.0.0/tests/vectors/response/RS-009-problem-409-replayed-refusal.json +63 -0
  164. anis_partners-1.0.0/tests/vectors/response/RS-010-invoice-reveal-200.json +62 -0
  165. anis_partners-1.0.0/tests/vectors/response/RS-101-signed-by-next-key.json +61 -0
  166. anis_partners-1.0.0/tests/vectors/response/RS-102-signed-by-retiring-key.json +61 -0
  167. anis_partners-1.0.0/tests/vectors/response/RS-103-key-document-carries-private-member.json +62 -0
  168. anis_partners-1.0.0/tests/vectors/response/RS-104-private-member-on-an-unused-key.json +62 -0
  169. anis_partners-1.0.0/tests/vectors/response/RS-105-published-key-coordinate-not-32-bytes.json +61 -0
  170. anis_partners-1.0.0/tests/vectors/safety-code/SC-01.json +185 -0
  171. anis_partners-1.0.0/tests/vectors/safety-code/SC-02.json +150 -0
  172. anis_partners-1.0.0/tests/vectors/safety-code/SC-03.json +170 -0
  173. anis_partners-1.0.0/tests/vectors/safety-code/SC-04.json +170 -0
  174. anis_partners-1.0.0/tests/vectors/safety-code/manifest.json +42 -0
  175. anis_partners-1.0.0/tests/verification/__init__.py +1 -0
  176. anis_partners-1.0.0/tests/verification/test_async_http_signing_key_source.py +409 -0
  177. anis_partners-1.0.0/tests/verification/test_http_signing_key_source.py +334 -0
  178. anis_partners-1.0.0/tests/verification/test_response_key.py +281 -0
  179. anis_partners-1.0.0/tools/__init__.py +1 -0
  180. anis_partners-1.0.0/tools/generate_errors.py +102 -0
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ build/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ .DS_Store
10
+ .idea/
11
+ .vscode/
12
+ samples/console/work/
@@ -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
+ }