solvapay-python 0.8.0__tar.gz → 0.9.1__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.
- solvapay_python-0.9.1/.github/dependabot.yml +18 -0
- solvapay_python-0.9.1/.github/workflows/ci.yml +81 -0
- solvapay_python-0.9.1/.github/workflows/contract.yml +22 -0
- solvapay_python-0.9.1/.github/workflows/docs.yml +21 -0
- solvapay_python-0.9.1/.github/workflows/publish.yml +32 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.gitignore +2 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CHANGELOG.md +49 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CONTRIBUTING.md +15 -2
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/PKG-INFO +69 -11
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/README.md +61 -6
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/SECURITY.md +15 -2
- solvapay_python-0.9.1/changelog.d/README.md +35 -0
- solvapay_python-0.9.1/docs/errors.md +50 -0
- solvapay_python-0.9.1/docs/guides/fastapi.md +53 -0
- solvapay_python-0.9.1/docs/guides/langchain.md +45 -0
- solvapay_python-0.9.1/docs/guides/mcp.md +50 -0
- solvapay_python-0.9.1/docs/idempotency.md +83 -0
- solvapay_python-0.9.1/docs/index.md +63 -0
- solvapay_python-0.9.1/docs/migration.md +49 -0
- solvapay_python-0.9.1/docs/reference/client.md +5 -0
- solvapay_python-0.9.1/docs/reference/exceptions.md +3 -0
- solvapay_python-0.9.1/docs/reference/models.md +3 -0
- solvapay_python-0.9.1/docs/reference/paywall.md +9 -0
- solvapay_python-0.9.1/docs/reference/webhooks.md +15 -0
- solvapay_python-0.9.1/docs/rfcs/0002-openapi-investigation.md +35 -0
- solvapay_python-0.9.1/docs/webhooks.md +79 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/uv.lock +128 -122
- solvapay_python-0.9.1/examples/multi-framework-paywall/uv.lock +1585 -0
- solvapay_python-0.9.1/mkdocs.yml +44 -0
- solvapay_python-0.9.1/pyproject.toml +132 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/__init__.py +1 -1
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_async_client.py +21 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/httpx_transport.py +5 -1
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/middleware.py +177 -1
- solvapay_python-0.9.1/src/solvapay/adapters/asgi.py +119 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/adapters/mcp.py +1 -1
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/client.py +2 -0
- solvapay_python-0.9.1/src/solvapay/idempotency.py +37 -0
- solvapay_python-0.9.1/src/solvapay/webhooks/__init__.py +43 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/pipeline.py +3 -14
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/replay.py +32 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/rotation.py +8 -4
- solvapay_python-0.9.1/src/solvapay/webhooks/sign.py +37 -0
- solvapay_python-0.9.1/tests/_transport/test_recording_transport.py +68 -0
- solvapay_python-0.9.1/tests/_transport/test_retry_transport.py +78 -0
- solvapay_python-0.9.1/tests/adapters/test_asgi.py +91 -0
- solvapay_python-0.9.1/tests/contract/README.md +38 -0
- solvapay_python-0.9.1/tests/contract/test_contract_ops.py +61 -0
- solvapay_python-0.9.1/tests/paywall/__init__.py +0 -0
- solvapay_python-0.9.1/tests/test_api_version.py +57 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_idempotency.py +33 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_paywall_state.py +42 -39
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_redaction.py +37 -0
- solvapay_python-0.9.1/tests/test_webhook_timing.py +82 -0
- solvapay_python-0.9.1/tests/webhooks/__init__.py +0 -0
- solvapay_python-0.9.1/tests/webhooks/test_async_cache.py +48 -0
- solvapay_python-0.9.1/tests/webhooks/test_rotation.py +47 -0
- solvapay_python-0.9.1/tests/webhooks/test_sign.py +37 -0
- solvapay_python-0.9.1/tools/lint_invariants.py +208 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/uv.lock +1315 -32
- solvapay_python-0.8.0/.github/workflows/ci.yml +0 -27
- solvapay_python-0.8.0/.github/workflows/publish.yml +0 -15
- solvapay_python-0.8.0/pyproject.toml +0 -73
- solvapay_python-0.8.0/src/solvapay/idempotency.py +0 -16
- solvapay_python-0.8.0/src/solvapay/webhooks/__init__.py +0 -28
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.python-version +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CODEOWNERS +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CODE_OF_CONDUCT.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/LICENSE +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/SUPPORT.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/assets/agent-marketplace.png +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/docs/architecture/layers.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/docs/rfcs/0001-spending-policy.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/.env.example +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/.gitignore +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/README.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/claim.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/pyproject.toml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/server.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/.env.example +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/.gitignore +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/README.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/agent.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/pyproject.toml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/.env.example +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/.gitignore +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/.streamlit/config.toml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/PLAN.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/README.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/agents.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/app.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/demo_customers.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/requirements.txt +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/sdk_gateway.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/ui_components.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/.env.example +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/.gitignore +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/README.md +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/agent_langchain.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/model.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/pyproject.toml +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/script_async.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/server_mcp.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/tool.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_config.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_http.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_stability.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/_recipe.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/adapters/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/adapters/langchain.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/events.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/exceptions.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/fastapi.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/langchain.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/models.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/_registry.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/checkout.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/customers.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/limits.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/merchant.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/plans.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/products.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/purchases.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/usage.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/core.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/decorators.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/meta.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/policy.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/resolvers.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/state.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall_state.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/py.typed +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/envelope.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/verify.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_stability/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_stability/test_stable_returns_identity.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_aclose_cascade.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_error_wrapping.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_headers_case_insensitive.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_middleware_composition.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_protocol_conformance.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/adapters/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/adapters/test_langchain_protocol.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/adapters/test_mcp.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/conftest.py +0 -0
- {solvapay_python-0.8.0/tests/operations → solvapay_python-0.9.1/tests/contract}/__init__.py +0 -0
- /solvapay_python-0.8.0/tests/paywall/__init__.py → /solvapay_python-0.9.1/tests/contract/cassettes/.gitkeep +0 -0
- {solvapay_python-0.8.0/tests/webhooks → solvapay_python-0.9.1/tests/operations}/__init__.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/operations/test_namespace_api.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/operations/test_path_interpolation.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/operations/test_retry_safety_enum.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_checkout_mint_error_surfaces.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_payable_tool_meta.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_resolvers.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_split_classes.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_admin.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_async_client.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_checkout.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_config.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_customer.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_errors.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_http.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_invariants.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_langchain.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_lifecycle.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_limits.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_packaging.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_paywall.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_webhook_events.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_webhooks.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/webhooks/test_clock_skew_vs_replay_ttl.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/webhooks/test_seen_cache_atomic.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/webhooks/test_webhook_pipeline.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tools/api_baseline.json +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tools/api_diff.py +0 -0
- {solvapay_python-0.8.0 → solvapay_python-0.9.1}/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,81 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 7 * * *" # nightly security sweep
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
security:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: astral-sh/setup-uv@v7
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
- run: uv python install 3.12
|
|
19
|
+
- run: uv sync --all-extras --dev
|
|
20
|
+
- name: bandit (high-severity, high-confidence only)
|
|
21
|
+
run: uv run bandit -r src/ -lll
|
|
22
|
+
- name: Install osv-scanner
|
|
23
|
+
run: |
|
|
24
|
+
curl -fsSL -o /tmp/osv-scanner.tar.gz \
|
|
25
|
+
https://github.com/google/osv-scanner/releases/download/v1.9.2/osv-scanner_linux_amd64.tar.gz
|
|
26
|
+
mkdir -p /tmp/osv && tar -xzf /tmp/osv-scanner.tar.gz -C /tmp/osv
|
|
27
|
+
sudo mv /tmp/osv/osv-scanner /usr/local/bin/osv-scanner
|
|
28
|
+
osv-scanner --version
|
|
29
|
+
- name: osv-scanner (lockfile vuln DB cross-check)
|
|
30
|
+
run: osv-scanner --lockfile=uv.lock
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
test:
|
|
34
|
+
strategy:
|
|
35
|
+
matrix:
|
|
36
|
+
include:
|
|
37
|
+
# Linux — full Python matrix
|
|
38
|
+
- os: ubuntu-latest
|
|
39
|
+
python-version: "3.10"
|
|
40
|
+
- os: ubuntu-latest
|
|
41
|
+
python-version: "3.11"
|
|
42
|
+
- os: ubuntu-latest
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
# macOS — 3.12 only
|
|
45
|
+
- os: macos-latest
|
|
46
|
+
python-version: "3.12"
|
|
47
|
+
# Windows — 3.12 only
|
|
48
|
+
- os: windows-latest
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
runs-on: ${{ matrix.os }}
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v6
|
|
53
|
+
with:
|
|
54
|
+
fetch-depth: 0
|
|
55
|
+
- uses: astral-sh/setup-uv@v7
|
|
56
|
+
with:
|
|
57
|
+
enable-cache: true
|
|
58
|
+
- name: Set Python ${{ matrix.python-version }}
|
|
59
|
+
run: uv python install ${{ matrix.python-version }}
|
|
60
|
+
- run: uv sync --all-extras --dev
|
|
61
|
+
- run: uv run ruff check src tests
|
|
62
|
+
- run: uv run ruff format --check src tests
|
|
63
|
+
- run: uv run mypy src
|
|
64
|
+
- run: uv run pytest -v
|
|
65
|
+
- run: uv run python tools/api_diff.py
|
|
66
|
+
- run: uv run lint-imports --config tools/importlinter.cfg
|
|
67
|
+
- run: uv run python tools/lint_invariants.py
|
|
68
|
+
- name: pip-audit
|
|
69
|
+
run: uv run pip-audit
|
|
70
|
+
- name: Check changelog fragment on PRs touching src/
|
|
71
|
+
if: github.event_name == 'pull_request'
|
|
72
|
+
shell: bash
|
|
73
|
+
run: |
|
|
74
|
+
git fetch origin ${{ github.base_ref }}
|
|
75
|
+
changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
|
76
|
+
if echo "$changed" | grep -q "^src/"; then
|
|
77
|
+
if ! echo "$changed" | grep -q "^changelog.d/"; then
|
|
78
|
+
echo "ERROR: src/ modified without a changelog.d/ fragment. Add one — see changelog.d/README.md"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
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@v6
|
|
13
|
+
- uses: astral-sh/setup-uv@v7
|
|
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@v6
|
|
16
|
+
- uses: astral-sh/setup-uv@v7
|
|
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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
jobs:
|
|
6
|
+
publish:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
environment: pypi
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
contents: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- uses: astral-sh/setup-uv@v7
|
|
15
|
+
- run: uv build
|
|
16
|
+
- name: Generate CycloneDX SBOM
|
|
17
|
+
run: |
|
|
18
|
+
# Resolve runtime + all-extras deps from the lockfile and emit a CycloneDX SBOM.
|
|
19
|
+
uv export --no-dev --all-extras --no-emit-project --format requirements-txt > /tmp/sbom-requirements.txt
|
|
20
|
+
uv tool run --from cyclonedx-bom cyclonedx-py requirements \
|
|
21
|
+
--pyproject pyproject.toml \
|
|
22
|
+
--of JSON --sv 1.6 \
|
|
23
|
+
-o sbom.cdx.json \
|
|
24
|
+
/tmp/sbom-requirements.txt
|
|
25
|
+
- name: Publish to PyPI (Sigstore-signed via trusted publishing)
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
27
|
+
with:
|
|
28
|
+
attestations: true
|
|
29
|
+
- name: Attach SBOM to GitHub release
|
|
30
|
+
env:
|
|
31
|
+
GH_TOKEN: ${{ github.token }}
|
|
32
|
+
run: gh release upload "${GITHUB_REF_NAME}" sbom.cdx.json --clobber
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.1] — 2026-06-05
|
|
4
|
+
|
|
5
|
+
Security & supply-chain quality patch (no public API change).
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- ``publish.yml``: PyPI uploads now ship PEP 740 attestations (Sigstore-backed via OIDC trusted publishing) and attach a CycloneDX SBOM (``sbom.cdx.json``) to each GitHub release.
|
|
10
|
+
- CI: new ``security`` job runs ``bandit -r src/ -lll`` (high-confidence high-severity only) and ``osv-scanner --lockfile=uv.lock`` as a cross-DB check against ``pip-audit``. Runs on PR and nightly cron.
|
|
11
|
+
- ``tests/test_redaction.py``: property-based test with Hypothesis (100 derandomized cases) drives random API keys, hex tokens, and webhook secrets through the HTTP transport and asserts the secret never appears in ``caplog.text`` at any log level.
|
|
12
|
+
- ``tests/test_webhook_timing.py``: smoke test that compares ``verify_webhook`` perf-counter distributions for perfect-match vs near-miss signatures, regressing if someone swaps ``hmac.compare_digest`` for ``==``.
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- ``pyproject.toml``: tighten ``httpx`` to ``<0.29`` (closes deviation: was unbounded). FastAPI extra remains unbounded by design — documented in-file.
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
- ``SECURITY.md``: explicit PCI-scope section — the SDK never transmits raw cardholder data; tokenization is server-side.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 0.9.0 — 2026-05-23
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **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)
|
|
30
|
+
- **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)
|
|
31
|
+
- **Webhook secret rotation**: `MultiSecretVerifier` full implementation — tries primary then secondary on signature mismatch; both comparisons constant-time. (HLD V1.7)
|
|
32
|
+
- **`AsyncInMemorySeenEventCache`**: async replay-dedup cache using `asyncio.Lock` for async webhook pipelines. (HLD V1.7)
|
|
33
|
+
- **`sign_webhook(body, secret, *, timestamp)`**: public helper to produce a valid `sv-signature` header for testing and outbound webhook fanout.
|
|
34
|
+
- **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)
|
|
35
|
+
- **`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]`)
|
|
36
|
+
- **`RecordingTransport`**: records `(RequestSpec, ResponseSpec)` pairs to JSON cassettes; replays on subsequent runs. Enables offline contract tests. (HLD V1.4)
|
|
37
|
+
- **Sandbox contract tests** (`tests/contract/`): one test per op against real sandbox via `RecordingTransport`. Nightly CI workflow (`contract.yml`).
|
|
38
|
+
- **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.
|
|
39
|
+
- **Towncrier changelog automation**: `changelog.d/` fragments, PR gate requiring a fragment when `src/` changes, `towncrier>=23` dev dep.
|
|
40
|
+
- **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)
|
|
41
|
+
- **Dependabot**: weekly Python + GitHub Actions dependency updates (`.github/dependabot.yml`).
|
|
42
|
+
- **`pip-audit` in CI**: dependency CVE scan on every push.
|
|
43
|
+
- **CI matrix expanded**: Ubuntu 3.10/3.11/3.12 + macOS 3.12 + Windows 3.12. (HLD V1.15)
|
|
44
|
+
- **`ResourceWarning` on unclosed `AsyncSolvaPay`**: `__del__` emits `ResourceWarning` if client is not closed and event loop is still running.
|
|
45
|
+
- **OpenAPI investigation RFC**: `docs/rfcs/0002-openapi-investigation.md`.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- `pytest.ini_options.filterwarnings`: adds `ignore::DeprecationWarning:solvapay` so tests exercising deprecated flat-shim API do not fail on expected warnings.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
3
52
|
## 0.8.0 — 2026-05-23
|
|
4
53
|
|
|
5
54
|
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
|
-
- [ ] `
|
|
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.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -21,14 +21,17 @@ Classifier: Topic :: Office/Business :: Financial
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: httpx
|
|
25
|
-
Requires-Dist: pydantic<2.
|
|
24
|
+
Requires-Dist: httpx<0.29,>=0.27
|
|
25
|
+
Requires-Dist: pydantic<2.14,>=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
|
-
Requires-Dist: langchain-core<
|
|
30
|
+
Requires-Dist: langchain-core<1.5,>=0.3; extra == 'langchain'
|
|
30
31
|
Provides-Extra: mcp
|
|
31
|
-
Requires-Dist: fastmcp<
|
|
32
|
+
Requires-Dist: fastmcp<4,>=3.2.0; 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
|
|
@@ -135,16 +138,23 @@ if limits.within_limits:
|
|
|
135
138
|
|
|
136
139
|
## Idempotency
|
|
137
140
|
|
|
138
|
-
All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys
|
|
141
|
+
All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys:
|
|
139
142
|
|
|
140
143
|
```python
|
|
141
144
|
from solvapay.idempotency import from_payload
|
|
142
145
|
|
|
146
|
+
# Default: key includes UTC date — rolls at midnight, bounds replay past server TTL
|
|
143
147
|
key = from_payload("track_usage", customer_ref, product_ref, "requests", units)
|
|
144
148
|
sv.usage.track(..., idempotency_key=key) # retry-safe
|
|
149
|
+
|
|
150
|
+
# Hourly bucket (high-frequency ops)
|
|
151
|
+
key = from_payload("charge", customer_ref, time_bucket="hour")
|
|
152
|
+
|
|
153
|
+
# Pure payload hash — caller manages TTL externally
|
|
154
|
+
key = from_payload("idempotent_op", ref, time_bucket=None)
|
|
145
155
|
```
|
|
146
156
|
|
|
147
|
-
Retried POSTs **must reuse the same key**.
|
|
157
|
+
Retried POSTs **must reuse the same key**. A bucket roll (midnight / hour boundary) produces a new key — the server treats it as a new request.
|
|
148
158
|
|
|
149
159
|
---
|
|
150
160
|
|
|
@@ -171,7 +181,7 @@ except SolvaPayError as e:
|
|
|
171
181
|
... # catch-all
|
|
172
182
|
```
|
|
173
183
|
|
|
174
|
-
No built-in retries by
|
|
184
|
+
No built-in retries by default. `solvapay[retry]` ships `RetryTransport` — exponential backoff with jitter, 3 attempts, respects `OpSpec.retry_safety` (won't retry non-idempotent ops without an idempotency key). Or layer `tenacity` manually.
|
|
175
185
|
|
|
176
186
|
---
|
|
177
187
|
|
|
@@ -190,6 +200,29 @@ pipeline = WebhookPipeline(
|
|
|
190
200
|
envelope = pipeline.process(body=request.body, signature=request.headers["sv-signature"])
|
|
191
201
|
```
|
|
192
202
|
|
|
203
|
+
**Secret rotation** — pass multiple secrets; primary tried first, secondary on mismatch:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
pipeline = WebhookPipeline(["whsec_new...", "whsec_old..."])
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Sign a webhook** (testing / outbound fanout):
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from solvapay.webhooks import sign_webhook
|
|
213
|
+
|
|
214
|
+
header = sign_webhook(body=b'{"type":"purchase.created"}', secret="whsec_...")
|
|
215
|
+
# → "t=1716470000,v1=abc123..."
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**ASGI adapter** — mount directly in FastAPI / Starlette / Litestar:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from solvapay.adapters.asgi import webhook_app
|
|
222
|
+
|
|
223
|
+
app.mount("/webhook", webhook_app(pipeline, on_event=handle))
|
|
224
|
+
```
|
|
225
|
+
|
|
193
226
|
**Typed events** — discriminated union over 13 event types:
|
|
194
227
|
|
|
195
228
|
```python
|
|
@@ -276,6 +309,8 @@ pip install solvapay-python # core
|
|
|
276
309
|
pip install 'solvapay-python[mcp]' # + FastMCP adapter (FastMCP ≥0.4)
|
|
277
310
|
pip install 'solvapay-python[langchain]' # + LangChain adapter (langchain-core ≥0.3)
|
|
278
311
|
pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
|
|
312
|
+
pip install 'solvapay-python[asgi]' # + raw ASGI webhook adapter (no extra deps)
|
|
313
|
+
pip install 'solvapay-python[retry]' # + RetryTransport (tenacity)
|
|
279
314
|
```
|
|
280
315
|
|
|
281
316
|
## Environment variables
|
|
@@ -299,17 +334,40 @@ pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
|
|
|
299
334
|
|
|
300
335
|
---
|
|
301
336
|
|
|
337
|
+
## API version pinning
|
|
338
|
+
|
|
339
|
+
Pin the API version your code was written against — prevents silent breakage when the server evolves:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
sv = SolvaPay(api_version="2026-05-22") # sends Solvapay-Version header
|
|
343
|
+
sv = SolvaPay(api_version=None) # omit header (use server default)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Default is `"2026-05-22"` (v0.9 ship date). Bump only on major SDK versions.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
302
350
|
## Roadmap
|
|
303
351
|
|
|
304
352
|
| Version | Theme |
|
|
305
353
|
|---------|-------|
|
|
306
354
|
| v0.8 ✅ | V1 architecture spine — Transport kernel, OpSpec registry, paywall/webhook packages, `@payable_tool`, stability manifest, layer DAG CI gate |
|
|
307
|
-
| v0.9 | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, contract tests, lint automation,
|
|
308
|
-
|
|
|
355
|
+
| v0.9 ✅ | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, ASGI adapter, secret rotation, `sign_webhook`, contract tests, lint automation, MkDocs site, supply-chain hygiene |
|
|
356
|
+
| v0.9.1 ✅ | Security & supply-chain quality — PyPI attestations (PEP 740 / Sigstore), CycloneDX SBOM on releases, `bandit` + `osv-scanner` CI, Hypothesis-driven secret-redaction property tests, constant-time `verify_webhook` smoke test, explicit PCI-scope statement |
|
|
357
|
+
| v1.0 | Gated on founder signal — OpenAPI-generated models, WSGI/Lambda adapters, V2 planning |
|
|
309
358
|
|
|
310
359
|
---
|
|
311
360
|
|
|
312
361
|
## Status
|
|
313
362
|
|
|
314
|
-
**v0.
|
|
363
|
+
**v0.9.1** — Security & supply-chain quality patch. PyPI uploads now carry PEP 740 attestations (Sigstore-backed via OIDC trusted publishing); each GitHub release ships a CycloneDX SBOM (`sbom.cdx.json`). `mypy --strict` clean (45 files). 302 tests. 87% line / 85% branch coverage.
|
|
364
|
+
|
|
365
|
+
**Supply chain & security posture:**
|
|
366
|
+
- PyPI trusted publishing with PEP 740 attestations (Sigstore)
|
|
367
|
+
- CycloneDX 1.6 SBOM attached to every release
|
|
368
|
+
- CI runs `bandit -r src/ -lll`, `osv-scanner --lockfile=uv.lock`, and `pip-audit` (cross-DB vuln check) on PR and nightly cron
|
|
369
|
+
- Constant-time HMAC comparison (`hmac.compare_digest`) regression-smoked
|
|
370
|
+
- Hypothesis-driven property tests assert no API key, hex token, or webhook secret leaks into log output at any level
|
|
371
|
+
- Tightened upper bounds on volatile deps (`httpx<0.29`, `pydantic<2.14`, `langchain-core<1.5`)
|
|
372
|
+
|
|
315
373
|
Community SDK, not official. Proposal: [solvapay/solvapay-sdk#187](https://github.com/solvapay/solvapay-sdk/issues/187).
|
|
@@ -102,16 +102,23 @@ if limits.within_limits:
|
|
|
102
102
|
|
|
103
103
|
## Idempotency
|
|
104
104
|
|
|
105
|
-
All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys
|
|
105
|
+
All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys:
|
|
106
106
|
|
|
107
107
|
```python
|
|
108
108
|
from solvapay.idempotency import from_payload
|
|
109
109
|
|
|
110
|
+
# Default: key includes UTC date — rolls at midnight, bounds replay past server TTL
|
|
110
111
|
key = from_payload("track_usage", customer_ref, product_ref, "requests", units)
|
|
111
112
|
sv.usage.track(..., idempotency_key=key) # retry-safe
|
|
113
|
+
|
|
114
|
+
# Hourly bucket (high-frequency ops)
|
|
115
|
+
key = from_payload("charge", customer_ref, time_bucket="hour")
|
|
116
|
+
|
|
117
|
+
# Pure payload hash — caller manages TTL externally
|
|
118
|
+
key = from_payload("idempotent_op", ref, time_bucket=None)
|
|
112
119
|
```
|
|
113
120
|
|
|
114
|
-
Retried POSTs **must reuse the same key**.
|
|
121
|
+
Retried POSTs **must reuse the same key**. A bucket roll (midnight / hour boundary) produces a new key — the server treats it as a new request.
|
|
115
122
|
|
|
116
123
|
---
|
|
117
124
|
|
|
@@ -138,7 +145,7 @@ except SolvaPayError as e:
|
|
|
138
145
|
... # catch-all
|
|
139
146
|
```
|
|
140
147
|
|
|
141
|
-
No built-in retries by
|
|
148
|
+
No built-in retries by default. `solvapay[retry]` ships `RetryTransport` — exponential backoff with jitter, 3 attempts, respects `OpSpec.retry_safety` (won't retry non-idempotent ops without an idempotency key). Or layer `tenacity` manually.
|
|
142
149
|
|
|
143
150
|
---
|
|
144
151
|
|
|
@@ -157,6 +164,29 @@ pipeline = WebhookPipeline(
|
|
|
157
164
|
envelope = pipeline.process(body=request.body, signature=request.headers["sv-signature"])
|
|
158
165
|
```
|
|
159
166
|
|
|
167
|
+
**Secret rotation** — pass multiple secrets; primary tried first, secondary on mismatch:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
pipeline = WebhookPipeline(["whsec_new...", "whsec_old..."])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Sign a webhook** (testing / outbound fanout):
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from solvapay.webhooks import sign_webhook
|
|
177
|
+
|
|
178
|
+
header = sign_webhook(body=b'{"type":"purchase.created"}', secret="whsec_...")
|
|
179
|
+
# → "t=1716470000,v1=abc123..."
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**ASGI adapter** — mount directly in FastAPI / Starlette / Litestar:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from solvapay.adapters.asgi import webhook_app
|
|
186
|
+
|
|
187
|
+
app.mount("/webhook", webhook_app(pipeline, on_event=handle))
|
|
188
|
+
```
|
|
189
|
+
|
|
160
190
|
**Typed events** — discriminated union over 13 event types:
|
|
161
191
|
|
|
162
192
|
```python
|
|
@@ -243,6 +273,8 @@ pip install solvapay-python # core
|
|
|
243
273
|
pip install 'solvapay-python[mcp]' # + FastMCP adapter (FastMCP ≥0.4)
|
|
244
274
|
pip install 'solvapay-python[langchain]' # + LangChain adapter (langchain-core ≥0.3)
|
|
245
275
|
pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
|
|
276
|
+
pip install 'solvapay-python[asgi]' # + raw ASGI webhook adapter (no extra deps)
|
|
277
|
+
pip install 'solvapay-python[retry]' # + RetryTransport (tenacity)
|
|
246
278
|
```
|
|
247
279
|
|
|
248
280
|
## Environment variables
|
|
@@ -266,17 +298,40 @@ pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
|
|
|
266
298
|
|
|
267
299
|
---
|
|
268
300
|
|
|
301
|
+
## API version pinning
|
|
302
|
+
|
|
303
|
+
Pin the API version your code was written against — prevents silent breakage when the server evolves:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
sv = SolvaPay(api_version="2026-05-22") # sends Solvapay-Version header
|
|
307
|
+
sv = SolvaPay(api_version=None) # omit header (use server default)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Default is `"2026-05-22"` (v0.9 ship date). Bump only on major SDK versions.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
269
314
|
## Roadmap
|
|
270
315
|
|
|
271
316
|
| Version | Theme |
|
|
272
317
|
|---------|-------|
|
|
273
318
|
| v0.8 ✅ | V1 architecture spine — Transport kernel, OpSpec registry, paywall/webhook packages, `@payable_tool`, stability manifest, layer DAG CI gate |
|
|
274
|
-
| v0.9 | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, contract tests, lint automation,
|
|
275
|
-
|
|
|
319
|
+
| v0.9 ✅ | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, ASGI adapter, secret rotation, `sign_webhook`, contract tests, lint automation, MkDocs site, supply-chain hygiene |
|
|
320
|
+
| v0.9.1 ✅ | Security & supply-chain quality — PyPI attestations (PEP 740 / Sigstore), CycloneDX SBOM on releases, `bandit` + `osv-scanner` CI, Hypothesis-driven secret-redaction property tests, constant-time `verify_webhook` smoke test, explicit PCI-scope statement |
|
|
321
|
+
| v1.0 | Gated on founder signal — OpenAPI-generated models, WSGI/Lambda adapters, V2 planning |
|
|
276
322
|
|
|
277
323
|
---
|
|
278
324
|
|
|
279
325
|
## Status
|
|
280
326
|
|
|
281
|
-
**v0.
|
|
327
|
+
**v0.9.1** — Security & supply-chain quality patch. PyPI uploads now carry PEP 740 attestations (Sigstore-backed via OIDC trusted publishing); each GitHub release ships a CycloneDX SBOM (`sbom.cdx.json`). `mypy --strict` clean (45 files). 302 tests. 87% line / 85% branch coverage.
|
|
328
|
+
|
|
329
|
+
**Supply chain & security posture:**
|
|
330
|
+
- PyPI trusted publishing with PEP 740 attestations (Sigstore)
|
|
331
|
+
- CycloneDX 1.6 SBOM attached to every release
|
|
332
|
+
- CI runs `bandit -r src/ -lll`, `osv-scanner --lockfile=uv.lock`, and `pip-audit` (cross-DB vuln check) on PR and nightly cron
|
|
333
|
+
- Constant-time HMAC comparison (`hmac.compare_digest`) regression-smoked
|
|
334
|
+
- Hypothesis-driven property tests assert no API key, hex token, or webhook secret leaks into log output at any level
|
|
335
|
+
- Tightened upper bounds on volatile deps (`httpx<0.29`, `pydantic<2.14`, `langchain-core<1.5`)
|
|
336
|
+
|
|
282
337
|
Community SDK, not official. Proposal: [solvapay/solvapay-sdk#187](https://github.com/solvapay/solvapay-sdk/issues/187).
|
|
@@ -13,8 +13,17 @@ This is a **community** Python SDK. It is not an official SolvaPay product.
|
|
|
13
13
|
- SolvaPay server-side vulnerabilities
|
|
14
14
|
- Issues requiring a SolvaPay account to reproduce
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
## PCI Scope
|
|
17
|
+
|
|
18
|
+
The SDK **never transmits raw cardholder data (PAN, CVV, expiry, track data)**.
|
|
19
|
+
All tokenization happens **server-side** at the SolvaPay API; the SDK only
|
|
20
|
+
exchanges SolvaPay API keys, customer references, and tokenized identifiers
|
|
21
|
+
over HTTPS. Integrators using this SDK do not bring PAN data into their
|
|
22
|
+
own application scope through it.
|
|
23
|
+
|
|
24
|
+
If you discover any code path that would cause raw cardholder data to traverse
|
|
25
|
+
the SDK boundary, treat it as a high-severity vulnerability and report it via
|
|
26
|
+
the channel below.
|
|
18
27
|
|
|
19
28
|
## Reporting a Vulnerability
|
|
20
29
|
|
|
@@ -25,6 +34,9 @@ Please include:
|
|
|
25
34
|
- Steps to reproduce
|
|
26
35
|
- Any proof-of-concept code (privately)
|
|
27
36
|
|
|
37
|
+
Encrypt sensitive payloads with the maintainer's public key on request.
|
|
38
|
+
Do **not** open public GitHub issues for suspected vulnerabilities.
|
|
39
|
+
|
|
28
40
|
**Response SLA:** Best-effort; typically within 7 days.
|
|
29
41
|
**Disclosure policy:** 90-day coordinated disclosure. Public CVE filed only for confirmed SDK bugs.
|
|
30
42
|
|
|
@@ -32,5 +44,6 @@ Please include:
|
|
|
32
44
|
|
|
33
45
|
| Version | Supported |
|
|
34
46
|
|---------|-----------|
|
|
47
|
+
| 0.9.x | Yes |
|
|
35
48
|
| 0.8.x | Yes |
|
|
36
49
|
| < 0.8 | No — upgrade recommended |
|
|
@@ -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.
|