solvapay-python 0.8.0__tar.gz → 0.9.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 (181) hide show
  1. solvapay_python-0.9.0/.github/dependabot.yml +18 -0
  2. solvapay_python-0.9.0/.github/workflows/ci.yml +53 -0
  3. solvapay_python-0.9.0/.github/workflows/contract.yml +22 -0
  4. solvapay_python-0.9.0/.github/workflows/docs.yml +21 -0
  5. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.gitignore +1 -0
  6. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/CHANGELOG.md +28 -0
  7. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/CONTRIBUTING.md +15 -2
  8. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/PKG-INFO +4 -1
  9. solvapay_python-0.9.0/changelog.d/README.md +35 -0
  10. solvapay_python-0.9.0/docs/errors.md +50 -0
  11. solvapay_python-0.9.0/docs/guides/fastapi.md +53 -0
  12. solvapay_python-0.9.0/docs/guides/langchain.md +45 -0
  13. solvapay_python-0.9.0/docs/guides/mcp.md +50 -0
  14. solvapay_python-0.9.0/docs/idempotency.md +83 -0
  15. solvapay_python-0.9.0/docs/index.md +63 -0
  16. solvapay_python-0.9.0/docs/migration.md +49 -0
  17. solvapay_python-0.9.0/docs/reference/client.md +5 -0
  18. solvapay_python-0.9.0/docs/reference/exceptions.md +3 -0
  19. solvapay_python-0.9.0/docs/reference/models.md +3 -0
  20. solvapay_python-0.9.0/docs/reference/paywall.md +9 -0
  21. solvapay_python-0.9.0/docs/reference/webhooks.md +15 -0
  22. solvapay_python-0.9.0/docs/rfcs/0002-openapi-investigation.md +35 -0
  23. solvapay_python-0.9.0/docs/webhooks.md +79 -0
  24. solvapay_python-0.9.0/mkdocs.yml +44 -0
  25. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/pyproject.toml +54 -1
  26. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/__init__.py +1 -1
  27. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_async_client.py +21 -0
  28. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_transport/httpx_transport.py +5 -1
  29. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_transport/middleware.py +177 -1
  30. solvapay_python-0.9.0/src/solvapay/adapters/asgi.py +119 -0
  31. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/adapters/mcp.py +1 -1
  32. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/client.py +2 -0
  33. solvapay_python-0.9.0/src/solvapay/idempotency.py +37 -0
  34. solvapay_python-0.9.0/src/solvapay/webhooks/__init__.py +43 -0
  35. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/webhooks/pipeline.py +3 -14
  36. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/webhooks/replay.py +32 -0
  37. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/webhooks/rotation.py +8 -4
  38. solvapay_python-0.9.0/src/solvapay/webhooks/sign.py +37 -0
  39. solvapay_python-0.9.0/tests/_transport/test_recording_transport.py +68 -0
  40. solvapay_python-0.9.0/tests/_transport/test_retry_transport.py +78 -0
  41. solvapay_python-0.9.0/tests/adapters/test_asgi.py +91 -0
  42. solvapay_python-0.9.0/tests/contract/README.md +38 -0
  43. solvapay_python-0.9.0/tests/contract/test_contract_ops.py +61 -0
  44. solvapay_python-0.9.0/tests/paywall/__init__.py +0 -0
  45. solvapay_python-0.9.0/tests/test_api_version.py +57 -0
  46. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_idempotency.py +33 -0
  47. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_paywall_state.py +42 -39
  48. solvapay_python-0.9.0/tests/webhooks/__init__.py +0 -0
  49. solvapay_python-0.9.0/tests/webhooks/test_async_cache.py +48 -0
  50. solvapay_python-0.9.0/tests/webhooks/test_rotation.py +47 -0
  51. solvapay_python-0.9.0/tests/webhooks/test_sign.py +37 -0
  52. solvapay_python-0.9.0/tools/lint_invariants.py +208 -0
  53. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/uv.lock +650 -7
  54. solvapay_python-0.8.0/.github/workflows/ci.yml +0 -27
  55. solvapay_python-0.8.0/src/solvapay/idempotency.py +0 -16
  56. solvapay_python-0.8.0/src/solvapay/webhooks/__init__.py +0 -28
  57. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  58. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  59. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
  60. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  61. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.github/workflows/publish.yml +0 -0
  62. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/.python-version +0 -0
  63. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/CODEOWNERS +0 -0
  64. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/CODE_OF_CONDUCT.md +0 -0
  65. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/LICENSE +0 -0
  66. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/README.md +0 -0
  67. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/SECURITY.md +0 -0
  68. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/SUPPORT.md +0 -0
  69. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/assets/agent-marketplace.png +0 -0
  70. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/docs/architecture/layers.md +0 -0
  71. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/docs/rfcs/0001-spending-policy.md +0 -0
  72. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/.env.example +0 -0
  73. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/.gitignore +0 -0
  74. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/README.md +0 -0
  75. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/claim.py +0 -0
  76. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/pyproject.toml +0 -0
  77. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/server.py +0 -0
  78. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/fastmcp-paywall/uv.lock +0 -0
  79. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/langchain-paywall/.env.example +0 -0
  80. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/langchain-paywall/.gitignore +0 -0
  81. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/langchain-paywall/README.md +0 -0
  82. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/langchain-paywall/agent.py +0 -0
  83. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/langchain-paywall/pyproject.toml +0 -0
  84. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/.env.example +0 -0
  85. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/.gitignore +0 -0
  86. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/.streamlit/config.toml +0 -0
  87. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/PLAN.md +0 -0
  88. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/README.md +0 -0
  89. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/agents.py +0 -0
  90. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/app.py +0 -0
  91. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/demo_customers.py +0 -0
  92. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/requirements.txt +0 -0
  93. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/sdk_gateway.py +0 -0
  94. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/marketplace/ui_components.py +0 -0
  95. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/.env.example +0 -0
  96. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/.gitignore +0 -0
  97. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/README.md +0 -0
  98. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/agent_langchain.py +0 -0
  99. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/model.py +0 -0
  100. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/pyproject.toml +0 -0
  101. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/script_async.py +0 -0
  102. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/server_mcp.py +0 -0
  103. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/examples/multi-framework-paywall/tool.py +0 -0
  104. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_config.py +0 -0
  105. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_http.py +0 -0
  106. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_stability.py +0 -0
  107. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_transport/__init__.py +0 -0
  108. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/_transport/_recipe.py +0 -0
  109. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/adapters/__init__.py +0 -0
  110. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/adapters/langchain.py +0 -0
  111. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/events.py +0 -0
  112. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/exceptions.py +0 -0
  113. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/fastapi.py +0 -0
  114. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/langchain.py +0 -0
  115. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/models.py +0 -0
  116. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/__init__.py +0 -0
  117. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/_registry.py +0 -0
  118. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/checkout.py +0 -0
  119. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/customers.py +0 -0
  120. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/limits.py +0 -0
  121. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/merchant.py +0 -0
  122. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/plans.py +0 -0
  123. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/products.py +0 -0
  124. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/purchases.py +0 -0
  125. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/operations/usage.py +0 -0
  126. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/__init__.py +0 -0
  127. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/core.py +0 -0
  128. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/decorators.py +0 -0
  129. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/meta.py +0 -0
  130. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/policy.py +0 -0
  131. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/resolvers.py +0 -0
  132. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall/state.py +0 -0
  133. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/paywall_state.py +0 -0
  134. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/py.typed +0 -0
  135. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/webhooks/envelope.py +0 -0
  136. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/src/solvapay/webhooks/verify.py +0 -0
  137. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/__init__.py +0 -0
  138. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_stability/__init__.py +0 -0
  139. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_stability/test_stable_returns_identity.py +0 -0
  140. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_transport/__init__.py +0 -0
  141. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_transport/test_aclose_cascade.py +0 -0
  142. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_transport/test_error_wrapping.py +0 -0
  143. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_transport/test_headers_case_insensitive.py +0 -0
  144. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_transport/test_middleware_composition.py +0 -0
  145. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/_transport/test_protocol_conformance.py +0 -0
  146. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/adapters/__init__.py +0 -0
  147. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/adapters/test_langchain_protocol.py +0 -0
  148. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/adapters/test_mcp.py +0 -0
  149. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/conftest.py +0 -0
  150. {solvapay_python-0.8.0/tests/operations → solvapay_python-0.9.0/tests/contract}/__init__.py +0 -0
  151. /solvapay_python-0.8.0/tests/paywall/__init__.py → /solvapay_python-0.9.0/tests/contract/cassettes/.gitkeep +0 -0
  152. {solvapay_python-0.8.0/tests/webhooks → solvapay_python-0.9.0/tests/operations}/__init__.py +0 -0
  153. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/operations/test_namespace_api.py +0 -0
  154. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/operations/test_path_interpolation.py +0 -0
  155. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/operations/test_retry_safety_enum.py +0 -0
  156. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/paywall/test_checkout_mint_error_surfaces.py +0 -0
  157. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/paywall/test_payable_tool_meta.py +0 -0
  158. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/paywall/test_resolvers.py +0 -0
  159. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/paywall/test_split_classes.py +0 -0
  160. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_admin.py +0 -0
  161. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_async_client.py +0 -0
  162. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_checkout.py +0 -0
  163. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_config.py +0 -0
  164. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_customer.py +0 -0
  165. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_errors.py +0 -0
  166. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_http.py +0 -0
  167. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_invariants.py +0 -0
  168. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_langchain.py +0 -0
  169. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_lifecycle.py +0 -0
  170. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_limits.py +0 -0
  171. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_packaging.py +0 -0
  172. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_paywall.py +0 -0
  173. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_redaction.py +0 -0
  174. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_webhook_events.py +0 -0
  175. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/test_webhooks.py +0 -0
  176. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/webhooks/test_clock_skew_vs_replay_ttl.py +0 -0
  177. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/webhooks/test_seen_cache_atomic.py +0 -0
  178. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tests/webhooks/test_webhook_pipeline.py +0 -0
  179. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tools/api_baseline.json +0 -0
  180. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tools/api_diff.py +0 -0
  181. {solvapay_python-0.8.0 → solvapay_python-0.9.0}/tools/importlinter.cfg +0 -0
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 5
8
+ labels:
9
+ - dependencies
10
+
11
+ - package-ecosystem: github-actions
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
15
+ open-pull-requests-limit: 5
16
+ labels:
17
+ - dependencies
18
+ - ci
@@ -0,0 +1,53 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ matrix:
12
+ include:
13
+ # Linux — full Python matrix
14
+ - os: ubuntu-latest
15
+ python-version: "3.10"
16
+ - os: ubuntu-latest
17
+ python-version: "3.11"
18
+ - os: ubuntu-latest
19
+ python-version: "3.12"
20
+ # macOS — 3.12 only
21
+ - os: macos-latest
22
+ python-version: "3.12"
23
+ # Windows — 3.12 only
24
+ - os: windows-latest
25
+ python-version: "3.12"
26
+ runs-on: ${{ matrix.os }}
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v3
30
+ with:
31
+ enable-cache: true
32
+ - name: Set Python ${{ matrix.python-version }}
33
+ run: uv python install ${{ matrix.python-version }}
34
+ - run: uv sync --all-extras --dev
35
+ - run: uv run ruff check src tests
36
+ - run: uv run ruff format --check src tests
37
+ - run: uv run mypy src
38
+ - run: uv run pytest -v
39
+ - run: uv run python tools/api_diff.py
40
+ - run: uv run lint-imports --config tools/importlinter.cfg
41
+ - run: uv run python tools/lint_invariants.py
42
+ - name: pip-audit
43
+ run: uv run pip-audit
44
+ - name: Check changelog fragment on PRs touching src/
45
+ if: github.event_name == 'pull_request'
46
+ run: |
47
+ changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
48
+ if echo "$changed" | grep -q "^src/"; then
49
+ if ! echo "$changed" | grep -q "^changelog.d/"; then
50
+ echo "ERROR: src/ modified without a changelog.d/ fragment. Add one — see changelog.d/README.md"
51
+ exit 1
52
+ fi
53
+ fi
@@ -0,0 +1,22 @@
1
+ name: contract
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 2 * * *" # nightly at 02:00 UTC
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ contract:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v3
14
+ with:
15
+ enable-cache: true
16
+ - run: uv sync --all-extras --dev
17
+ - name: Run contract tests
18
+ run: uv run pytest tests/contract/ -m contract -v
19
+ env:
20
+ SOLVAPAY_SANDBOX_KEY: ${{ secrets.SOLVAPAY_SANDBOX_KEY }}
21
+ SOLVAPAY_TEST_CUSTOMER_REF: ${{ secrets.SOLVAPAY_TEST_CUSTOMER_REF }}
22
+ SOLVAPAY_TEST_PRODUCT_REF: ${{ secrets.SOLVAPAY_TEST_PRODUCT_REF }}
@@ -0,0 +1,21 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v3
17
+ with:
18
+ enable-cache: true
19
+ - run: uv sync --group docs
20
+ - name: Deploy to GitHub Pages
21
+ run: uv run mkdocs gh-deploy --force
@@ -12,3 +12,4 @@ htmlcov/
12
12
  .env
13
13
  .env.local
14
14
  *.egg
15
+ .DS_Store
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-05-23
4
+
5
+ Production polish + C1 adversarial-review closure. API-version pinning, idempotency TTL, webhook secret rotation, ASGI adapter, retry/recording transports, contract tests, lint automation, doc site, supply-chain hardening.
6
+
7
+ ### Added
8
+ - **API-version pinning**: `SolvaPay(api_version="2026-05-22")` and `AsyncSolvaPay(api_version=...)` send `Solvapay-Version` header. Default pinned to `"2026-05-22"`. `api_version=None` omits the header. (HLD V1.13)
9
+ - **Idempotency time-bucket encoding**: `from_payload(*parts, time_bucket="day"|"hour"|None)`. Default `"day"` appends UTC date so keys roll at midnight, bounding replay ambiguity past server TTL. (HLD V1.14)
10
+ - **Webhook secret rotation**: `MultiSecretVerifier` full implementation — tries primary then secondary on signature mismatch; both comparisons constant-time. (HLD V1.7)
11
+ - **`AsyncInMemorySeenEventCache`**: async replay-dedup cache using `asyncio.Lock` for async webhook pipelines. (HLD V1.7)
12
+ - **`sign_webhook(body, secret, *, timestamp)`**: public helper to produce a valid `sv-signature` header for testing and outbound webhook fanout.
13
+ - **ASGI webhook adapter** (`solvapay[asgi]`): `webhook_app(pipeline, on_event, path)` returns a raw ASGI app mountable in Starlette, FastAPI, Litestar, or BlackSheep. (HLD V1.10)
14
+ - **`RetryTransport`**: middleware that retries `APIConnectionError`, `APITimeoutError`, `APIServerError`, `RateLimitError` — exponential backoff with jitter, max 3 attempts. Consults `OpSpec.retry_safety`; refuses to retry `NEVER` ops. (HLD V1.4, `solvapay[retry]`)
15
+ - **`RecordingTransport`**: records `(RequestSpec, ResponseSpec)` pairs to JSON cassettes; replays on subsequent runs. Enables offline contract tests. (HLD V1.4)
16
+ - **Sandbox contract tests** (`tests/contract/`): one test per op against real sandbox via `RecordingTransport`. Nightly CI workflow (`contract.yml`).
17
+ - **Lint invariants** (`tools/lint_invariants.py`): AST checks for 10 gotchas (future annotations, `hmac.compare_digest`, no `logging.basicConfig`, no `asyncio.run()`, alias discipline, etc.). Run in CI on every push.
18
+ - **Towncrier changelog automation**: `changelog.d/` fragments, PR gate requiring a fragment when `src/` changes, `towncrier>=23` dev dep.
19
+ - **MkDocs Material doc site**: `mkdocs.yml` + `docs/` skeleton (Quickstart, Reference, Architecture, Guides, Errors, Idempotency, Webhooks, Migration). Deploys to GitHub Pages on tag. (HLD V1.12)
20
+ - **Dependabot**: weekly Python + GitHub Actions dependency updates (`.github/dependabot.yml`).
21
+ - **`pip-audit` in CI**: dependency CVE scan on every push.
22
+ - **CI matrix expanded**: Ubuntu 3.10/3.11/3.12 + macOS 3.12 + Windows 3.12. (HLD V1.15)
23
+ - **`ResourceWarning` on unclosed `AsyncSolvaPay`**: `__del__` emits `ResourceWarning` if client is not closed and event loop is still running.
24
+ - **OpenAPI investigation RFC**: `docs/rfcs/0002-openapi-investigation.md`.
25
+
26
+ ### Changed
27
+ - `pytest.ini_options.filterwarnings`: adds `ignore::DeprecationWarning:solvapay` so tests exercising deprecated flat-shim API do not fail on expected warnings.
28
+
29
+ ---
30
+
3
31
  ## 0.8.0 — 2026-05-23
4
32
 
5
33
  V1 architecture spine + AI-agent moat. 10 atomic commits establishing locked architecture invariants per HLD §V1.1–V1.19.
@@ -30,13 +30,26 @@ The SDK has a strict layer hierarchy — see [docs/architecture/layers.md](docs/
30
30
 
31
31
  CI fails on violations via `import-linter`.
32
32
 
33
+ ## Changelog fragment (required)
34
+
35
+ Every PR that modifies `src/` must include a towncrier fragment in `changelog.d/`.
36
+
37
+ ```bash
38
+ # Create a fragment (replace 42 with your issue/PR number)
39
+ echo "Add RetryTransport middleware for automatic retry on transient errors." > changelog.d/42.feature
40
+ ```
41
+
42
+ See `changelog.d/README.md` for naming conventions and types.
43
+
44
+ PRs that touch `src/` without a `changelog.d/` entry will fail CI.
45
+
33
46
  ## PR checklist
34
47
 
35
48
  - [ ] Tests added for new behavior
36
- - [ ] `CHANGELOG.md` updated
49
+ - [ ] `changelog.d/<issue>.<type>` fragment added (required for `src/` changes)
37
50
  - [ ] `docs/` updated if public API changed
38
51
  - [ ] Layer DAG respected (`uv run lint-imports` passes)
39
- - [ ] Stability manifest updated if new public exports added
52
+ - [ ] Stability manifest updated if new public exports added (`uv run python tools/api_diff.py`)
40
53
 
41
54
  ## Commit style
42
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solvapay-python
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Community Python SDK for SolvaPay (agent-native payment rails)
5
5
  Project-URL: Homepage, https://github.com/dhruv-sanan/solvapay-python
6
6
  Project-URL: Issues, https://github.com/dhruv-sanan/solvapay-python/issues
@@ -23,12 +23,15 @@ Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: httpx>=0.27
25
25
  Requires-Dist: pydantic<2.13,>=2.6
26
+ Provides-Extra: asgi
26
27
  Provides-Extra: fastapi
27
28
  Requires-Dist: fastapi>=0.110; extra == 'fastapi'
28
29
  Provides-Extra: langchain
29
30
  Requires-Dist: langchain-core<0.4,>=0.3; extra == 'langchain'
30
31
  Provides-Extra: mcp
31
32
  Requires-Dist: fastmcp<0.5,>=0.4; extra == 'mcp'
33
+ Provides-Extra: retry
34
+ Requires-Dist: tenacity<10,>=8.2; extra == 'retry'
32
35
  Description-Content-Type: text/markdown
33
36
 
34
37
  # solvapay-python
@@ -0,0 +1,35 @@
1
+ # Changelog Fragments
2
+
3
+ This directory contains towncrier changelog fragments. Each file represents one change entry.
4
+
5
+ ## Fragment naming
6
+
7
+ ```
8
+ <issue_or_pr_number>.<type>
9
+ ```
10
+
11
+ **Types:** `feature`, `bugfix`, `doc`, `removal`
12
+
13
+ **Examples:**
14
+ ```
15
+ 42.feature → "Add RetryTransport middleware"
16
+ 55.bugfix → "Fix idempotency key not sent on retry"
17
+ 60.doc → "Document api_version pinning"
18
+ 99.removal → "Remove deprecated SolvaPayAPIError alias"
19
+ ```
20
+
21
+ ## Required for PRs
22
+
23
+ Any PR that touches `src/` **must** include at least one fragment. PRs without a fragment will fail CI.
24
+
25
+ Example fragment content:
26
+ ```
27
+ Add ``api_version`` kwarg to ``SolvaPay`` and ``AsyncSolvaPay`` for server-side API version pinning.
28
+ ```
29
+
30
+ ## Building the changelog
31
+
32
+ ```bash
33
+ uv run towncrier build --version 0.9.0 --draft # preview
34
+ uv run towncrier build --version 0.9.0 # write to CHANGELOG.md
35
+ ```
@@ -0,0 +1,50 @@
1
+ # Errors
2
+
3
+ ```
4
+ SolvaPayError
5
+ ├── APIError(status_code, body, request_id, error_code, error_message)
6
+ │ ├── AuthenticationError # 401
7
+ │ ├── PermissionError # 403
8
+ │ ├── NotFoundError # 404
9
+ │ ├── RateLimitError(retry_after) # 429
10
+ │ ├── InvalidRequestError # other 4xx
11
+ │ └── APIServerError # 5xx
12
+ ├── APIConnectionError # network failure
13
+ ├── APITimeoutError # timeout
14
+ └── PaywallRequired # paywall gate hit
15
+ .checkout_url: str | None
16
+ .checkout_mint_error: APIError | None
17
+ ```
18
+
19
+ ## Handling errors
20
+
21
+ ```python
22
+ from solvapay import SolvaPay
23
+ from solvapay.exceptions import (
24
+ AuthenticationError,
25
+ RateLimitError,
26
+ APIConnectionError,
27
+ PaywallRequired,
28
+ )
29
+
30
+ sv = SolvaPay()
31
+
32
+ try:
33
+ sv.limits.check(customer_ref="cus_1", product_ref="prd_1")
34
+ except AuthenticationError:
35
+ print("Invalid API key")
36
+ except RateLimitError as e:
37
+ print(f"Rate limited — retry after {e.retry_after}s")
38
+ except APIConnectionError:
39
+ print("Network failure — safe to retry with idempotency key")
40
+ except PaywallRequired as e:
41
+ print(f"Paywall hit. Checkout: {e.checkout_url}")
42
+ ```
43
+
44
+ ## `request_id`
45
+
46
+ Every `APIError` captures `request_id` from `x-request-id` or `x-correlation-id` response headers. Include this in support tickets.
47
+
48
+ ## Legacy alias
49
+
50
+ `SolvaPayAPIError` is an alias for `APIError`. It emits `DeprecationWarning` and will be removed in v2.0. Use `APIError` directly.
@@ -0,0 +1,53 @@
1
+ # FastAPI Integration
2
+
3
+ ## Webhook router
4
+
5
+ ```python
6
+ from fastapi import FastAPI
7
+ from solvapay.fastapi import webhook_router
8
+
9
+ app = FastAPI()
10
+ app.include_router(
11
+ webhook_router(
12
+ secret="whsec_...",
13
+ on_event=handle_event,
14
+ path="/webhook",
15
+ )
16
+ )
17
+ ```
18
+
19
+ ## ASGI webhook app (framework-agnostic)
20
+
21
+ ```python
22
+ from fastapi import FastAPI
23
+ from solvapay.adapters.asgi import webhook_app
24
+ from solvapay.webhooks import WebhookPipeline
25
+
26
+ pipeline = WebhookPipeline(secrets=["whsec_..."])
27
+
28
+ async def handle(envelope):
29
+ event = envelope.event
30
+ print(f"Received {event['type']} for {event.get('customerId')}")
31
+
32
+ app = FastAPI()
33
+ app.mount("/webhook", webhook_app(pipeline, handle))
34
+ ```
35
+
36
+ ## Paywall decorator
37
+
38
+ ```python
39
+ from fastapi import FastAPI, Request
40
+ from solvapay.paywall import require
41
+ from solvapay.exceptions import PaywallRequired
42
+
43
+ app = FastAPI()
44
+
45
+ @app.post("/api/search")
46
+ @require(product="prd_0QKI8NHF", plan="pln_pro", customer_ref_arg="customer_id")
47
+ async def search(customer_id: str, query: str):
48
+ return {"results": [...]}
49
+
50
+ @app.exception_handler(PaywallRequired)
51
+ async def paywall_handler(req: Request, exc: PaywallRequired):
52
+ return {"error": "upgrade_required", "checkout_url": exc.checkout_url}
53
+ ```
@@ -0,0 +1,45 @@
1
+ # LangChain Integration
2
+
3
+ Monetize any LangChain tool with SolvaPay.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install "solvapay-python[langchain]"
9
+ ```
10
+
11
+ ## Monetize a tool
12
+
13
+ ```python
14
+ from solvapay import SolvaPay
15
+ from solvapay.adapters.langchain import monetize_tool
16
+
17
+ sv = SolvaPay()
18
+
19
+ # Any LangChain BaseTool-compatible object
20
+ monetized = monetize_tool(
21
+ my_langchain_tool,
22
+ product="prd_0QKI8NHF",
23
+ customer_ref_arg="customer_ref",
24
+ client=sv,
25
+ )
26
+ ```
27
+
28
+ ## With `@payable_tool`
29
+
30
+ If a tool is already decorated with `@payable_tool`, `monetize_tool` reads `__solvapay_meta__` automatically:
31
+
32
+ ```python
33
+ from solvapay.paywall import payable_tool
34
+ from solvapay.adapters.langchain import monetize_tool
35
+
36
+ @payable_tool(product="prd_0QKI8NHF")
37
+ def web_search(*, customer_ref: str, query: str) -> list[str]: ...
38
+
39
+ monetized = monetize_tool(web_search, client=sv)
40
+ ```
41
+
42
+ ## See also
43
+
44
+ - `examples/langchain-paywall/` — standalone LangChain agent example
45
+ - `examples/multi-framework-paywall/` — same tool in MCP + LangChain + async
@@ -0,0 +1,50 @@
1
+ # MCP Integration
2
+
3
+ Monetize any MCP tool with SolvaPay using `@payable_tool`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install "solvapay-python[mcp]"
9
+ ```
10
+
11
+ ## Stamp a tool
12
+
13
+ ```python
14
+ from solvapay.paywall import payable_tool
15
+
16
+ @payable_tool(product="prd_0QKI8NHF")
17
+ def web_search(*, customer_ref: str, query: str) -> list[str]:
18
+ """Search the web."""
19
+ return [...]
20
+ ```
21
+
22
+ ## Register with FastMCP
23
+
24
+ ```python
25
+ from fastmcp import FastMCP
26
+ from solvapay.adapters.mcp import register_payable_tool_fastmcp
27
+
28
+ mcp = FastMCP("my-server")
29
+ register_payable_tool_fastmcp(mcp, web_search)
30
+ ```
31
+
32
+ ## Schema flavors
33
+
34
+ ```python
35
+ from solvapay.adapters.mcp import (
36
+ payable_tool_mcp_schema,
37
+ payable_tool_anthropic_tool,
38
+ payable_tool_openai_function,
39
+ payable_tool_langchain_args_schema,
40
+ )
41
+
42
+ mcp_schema = payable_tool_mcp_schema(web_search)
43
+ anthropic_tool = payable_tool_anthropic_tool(web_search)
44
+ openai_fn = payable_tool_openai_function(web_search)
45
+ langchain_schema = payable_tool_langchain_args_schema(web_search)
46
+ ```
47
+
48
+ ## Multi-framework example
49
+
50
+ See `examples/multi-framework-paywall/` for a complete example running one tool across FastMCP, LangChain, and raw async.
@@ -0,0 +1,83 @@
1
+ # Idempotency
2
+
3
+ Idempotency keys prevent duplicate operations when a request is retried after a network failure or timeout.
4
+
5
+ ## Quick start
6
+
7
+ ```python
8
+ from solvapay import SolvaPay
9
+ from solvapay.idempotency import from_payload
10
+
11
+ sv = SolvaPay()
12
+
13
+ key = from_payload("cus_123", "prd_abc", "checkout")
14
+ session = sv.checkout.create_session(
15
+ customer_ref="cus_123",
16
+ product_ref="prd_abc",
17
+ idempotency_key=key,
18
+ )
19
+ ```
20
+
21
+ ## `from_payload` — key generation
22
+
23
+ ```python
24
+ from solvapay.idempotency import from_payload
25
+
26
+ key = from_payload(*parts, time_bucket="day")
27
+ ```
28
+
29
+ Produces a 32-hex-character SHA-256 key from the given payload parts.
30
+
31
+ ### `time_bucket` parameter
32
+
33
+ | Value | Behaviour | Use when |
34
+ |-------|-----------|----------|
35
+ | `"day"` (default) | Appends current UTC date (`2026-05-22`). Key changes at midnight UTC. | Standard checkout / purchase flows |
36
+ | `"hour"` | Appends current UTC hour (`2026-05-22T14`). Key changes every hour. | High-frequency operations where 24 h TTL is too wide |
37
+ | `None` | Pure payload hash — deterministic across time. | Caller manages TTL externally, or operation has no server-side TTL |
38
+
39
+ ### Retry contract
40
+
41
+ **Retried POSTs MUST reuse the exact same `idempotency_key` as the original call.**
42
+
43
+ If the key changes between the original request and the retry, the server treats it as a new request, which can cause duplicate charges or records.
44
+
45
+ ```python
46
+ key = from_payload("cus_123", "prd_abc", time_bucket="day")
47
+
48
+ # Original attempt (may fail with network error)
49
+ try:
50
+ session = sv.checkout.create_session(
51
+ customer_ref="cus_123",
52
+ product_ref="prd_abc",
53
+ idempotency_key=key,
54
+ )
55
+ except APIConnectionError:
56
+ # Retry — same key, same day → server deduplicates
57
+ session = sv.checkout.create_session(
58
+ customer_ref="cus_123",
59
+ product_ref="prd_abc",
60
+ idempotency_key=key,
61
+ )
62
+ ```
63
+
64
+ ### Bucket roll
65
+
66
+ A bucket roll (midnight UTC for `"day"`, hour boundary for `"hour"`) produces a **different key**. The server will treat a request with the new key as a fresh operation. Do not retry across a bucket boundary unless you intend to create a new operation.
67
+
68
+ ## Which operations accept `idempotency_key`?
69
+
70
+ All mutating `POST` operations accept an optional `idempotency_key` kwarg:
71
+
72
+ - `sv.checkout.create_session(..., idempotency_key=key)`
73
+ - `sv.customers.ensure(..., idempotency_key=key)`
74
+ - `sv.usage.track(..., idempotency_key=key)`
75
+ - `sv.purchases.cancel(..., idempotency_key=key)`
76
+ - `sv.purchases.reactivate(..., idempotency_key=key)`
77
+ - `sv.products.create(..., idempotency_key=key)`
78
+ - `sv.products.clone(..., idempotency_key=key)`
79
+ - `sv.plans.create(..., idempotency_key=key)`
80
+
81
+ ## Server TTL
82
+
83
+ The SolvaPay server deduplicates idempotency keys within a TTL window. Keys presented after the TTL expires are treated as new requests. The `time_bucket="day"` default is conservative and safe for typical retry windows.
@@ -0,0 +1,63 @@
1
+ # solvapay-python
2
+
3
+ Community Python SDK for [SolvaPay](https://solvapay.com) — agent-native payment rails.
4
+
5
+ ```bash
6
+ pip install solvapay-python
7
+ ```
8
+
9
+ ## Quickstart
10
+
11
+ ```python
12
+ from solvapay import SolvaPay
13
+
14
+ sv = SolvaPay() # reads SOLVAPAY_SECRET_KEY env var
15
+
16
+ # Ensure a customer exists
17
+ customer_ref = sv.customers.ensure("user_123")
18
+
19
+ # Create a checkout session
20
+ session = sv.checkout.create_session(
21
+ customer_ref=customer_ref,
22
+ product_ref="prd_0QKI8NHF",
23
+ )
24
+ print(session.checkout_url)
25
+ ```
26
+
27
+ ## Async
28
+
29
+ ```python
30
+ from solvapay import AsyncSolvaPay
31
+
32
+ async with AsyncSolvaPay() as sv:
33
+ session = await sv.checkout.acreate_session(
34
+ customer_ref="cus_123",
35
+ product_ref="prd_0QKI8NHF",
36
+ )
37
+ ```
38
+
39
+ ## MCP (payable tools)
40
+
41
+ ```python
42
+ from solvapay.paywall import payable_tool
43
+
44
+ @payable_tool(product="prd_0QKI8NHF")
45
+ def web_search(*, customer_ref: str, query: str) -> list[str]:
46
+ ...
47
+ ```
48
+
49
+ ## API version pinning
50
+
51
+ ```python
52
+ sv = SolvaPay(api_version="2026-05-22") # pins Solvapay-Version header
53
+ sv = SolvaPay(api_version=None) # omits header
54
+ ```
55
+
56
+ ## Install extras
57
+
58
+ ```bash
59
+ pip install "solvapay-python[mcp]" # FastMCP adapter
60
+ pip install "solvapay-python[langchain]" # LangChain adapter
61
+ pip install "solvapay-python[retry]" # RetryTransport (tenacity)
62
+ pip install "solvapay-python[asgi]" # ASGI webhook adapter
63
+ ```
@@ -0,0 +1,49 @@
1
+ # Migration Guide
2
+
3
+ ## v0.7.x → v0.8.x
4
+
5
+ ### Flat methods are now deprecated
6
+
7
+ All flat methods on `SolvaPay` / `AsyncSolvaPay` (e.g. `sv.ensure_customer()`) now emit `DeprecationWarning` and will be **removed in v2.0**.
8
+
9
+ Migrate to the resource namespace API:
10
+
11
+ | Old (v0.7) | New (v0.8+) |
12
+ |---|---|
13
+ | `sv.ensure_customer(ref)` | `sv.customers.ensure(ref)` |
14
+ | `sv.get_customer(ref)` | `sv.customers.get(ref)` |
15
+ | `sv.check_limits(...)` | `sv.limits.check(...)` |
16
+ | `sv.create_checkout_session(...)` | `sv.checkout.create_session(...)` |
17
+ | `sv.track_usage(...)` | `sv.usage.track(...)` |
18
+ | `sv.cancel_purchase(ref)` | `sv.purchases.cancel(ref)` |
19
+ | `sv.reactivate_purchase(ref)` | `sv.purchases.reactivate(ref)` |
20
+
21
+ Async variants: prepend `a` to the method name, e.g. `sv.customers.aensure(ref)`.
22
+
23
+ ### Mock pattern changed
24
+
25
+ ```python
26
+ # Old (v0.7) — WRONG for v0.8
27
+ client = MagicMock(spec=SolvaPay)
28
+ client.check_limits.return_value = ...
29
+
30
+ # New (v0.8+) — CORRECT
31
+ client = MagicMock()
32
+ client.limits.check.return_value = ...
33
+ ```
34
+
35
+ ### `webhooks.py` and `paywall.py` are now packages
36
+
37
+ Imports from `solvapay.webhooks` and `solvapay.paywall` still work — the packages re-export everything at the same names.
38
+
39
+ ## v0.6.x → v0.7.x
40
+
41
+ ### Wire format fixes
42
+
43
+ - `Customer.customer_ref` now accepts `reference` (real API) and `customerRef` (legacy) via `AliasChoices`
44
+ - `BalanceResponse.balance` is now major-unit (dollars), not cents
45
+ - Use `paywall_state.gate()` instead of manual enrichment after `check_limits`
46
+
47
+ ## SolvaPayAPIError alias
48
+
49
+ `SolvaPayAPIError = APIError` is a deprecated alias. Use `APIError` directly. Will be removed in v2.0.
@@ -0,0 +1,5 @@
1
+ # Client Reference
2
+
3
+ ::: solvapay.client.SolvaPay
4
+
5
+ ::: solvapay._async_client.AsyncSolvaPay
@@ -0,0 +1,3 @@
1
+ # Exceptions Reference
2
+
3
+ ::: solvapay.exceptions
@@ -0,0 +1,3 @@
1
+ # Models Reference
2
+
3
+ ::: solvapay.models
@@ -0,0 +1,9 @@
1
+ # Paywall Reference
2
+
3
+ ::: solvapay.paywall.core.Paywall
4
+
5
+ ::: solvapay.paywall.core.AsyncPaywall
6
+
7
+ ::: solvapay.paywall.core.PaywallRequired
8
+
9
+ ::: solvapay.paywall.meta.PayableToolMeta