solvapay-python 0.9.2__tar.gz → 0.9.3__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.2 → solvapay_python-0.9.3}/CHANGELOG.md +7 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/PKG-INFO +4 -1
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/README.md +3 -0
- solvapay_python-0.9.3/docs/architecture/layers.md +84 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/pyproject.toml +1 -1
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/__init__.py +1 -1
- solvapay_python-0.9.3/tests/_baselines/cold_import.json +1 -0
- solvapay_python-0.9.3/tests/test_cold_import.py +92 -0
- solvapay_python-0.9.3/tests/test_layer_dag.py +35 -0
- solvapay_python-0.9.3/tools/importlinter.cfg +32 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/uv.lock +28 -2
- solvapay_python-0.9.2/docs/architecture/layers.md +0 -46
- solvapay_python-0.9.2/tests/_baselines/cold_import.json +0 -1
- solvapay_python-0.9.2/tests/test_cold_import.py +0 -66
- solvapay_python-0.9.2/tools/importlinter.cfg +0 -42
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/dependabot.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/workflows/ci.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/workflows/contract.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/workflows/docs.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.github/workflows/publish.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.gitignore +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.osv-scanner.toml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/.python-version +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/CODEOWNERS +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/CODE_OF_CONDUCT.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/CONTRIBUTING.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/LICENSE +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/SECURITY.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/SUPPORT.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/assets/agent-marketplace.png +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/changelog.d/README.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/architecture/cold-start.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/errors.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/guides/fastapi.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/guides/langchain.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/guides/mcp.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/idempotency.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/index.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/migration.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/reference/client.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/reference/exceptions.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/reference/models.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/reference/paywall.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/reference/webhooks.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/rfcs/0001-spending-policy.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/rfcs/0002-openapi-investigation.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/troubleshooting.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/docs/webhooks.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/.env.example +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/.gitignore +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/README.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/claim.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/pyproject.toml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/server.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/fastmcp-paywall/uv.lock +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/langchain-paywall/.env.example +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/langchain-paywall/.gitignore +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/langchain-paywall/README.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/langchain-paywall/agent.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/langchain-paywall/pyproject.toml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/.env.example +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/.gitignore +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/.streamlit/config.toml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/PLAN.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/README.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/agents.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/app.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/demo_customers.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/requirements.txt +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/sdk_gateway.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/marketplace/ui_components.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/.env.example +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/.gitignore +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/README.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/agent_langchain.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/model.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/pyproject.toml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/script_async.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/server_mcp.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/tool.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/examples/multi-framework-paywall/uv.lock +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/mkdocs.yml +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_async_client.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_config.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_http.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_stability.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_transport/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_transport/_recipe.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_transport/httpx_transport.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/_transport/middleware.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/adapters/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/adapters/asgi.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/adapters/langchain.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/adapters/mcp.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/client.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/events.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/exceptions.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/fastapi.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/idempotency.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/langchain.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/models.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/_registry.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/checkout.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/customers.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/limits.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/merchant.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/plans.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/products.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/purchases.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/operations/usage.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/core.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/decorators.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/meta.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/policy.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/resolvers.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall/state.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/paywall_state.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/py.typed +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/envelope.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/pipeline.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/replay.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/rotation.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/sign.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/src/solvapay/webhooks/verify.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_stability/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_stability/test_stable_returns_identity.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_aclose_cascade.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_error_wrapping.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_headers_case_insensitive.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_middleware_composition.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_protocol_conformance.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_recording_transport.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/_transport/test_retry_transport.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/adapters/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/adapters/test_asgi.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/adapters/test_langchain_protocol.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/adapters/test_mcp.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/benchmarks/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/benchmarks/test_benchmarks.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/conftest.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/contract/README.md +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/contract/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/contract/cassettes/.gitkeep +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/contract/test_contract_ops.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/operations/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/operations/test_namespace_api.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/operations/test_path_interpolation.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/operations/test_retry_safety_enum.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/paywall/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/paywall/test_checkout_mint_error_surfaces.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/paywall/test_payable_tool_meta.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/paywall/test_resolvers.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/paywall/test_split_classes.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_admin.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_api_version.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_async_client.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_checkout.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_config.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_customer.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_errors.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_http.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_idempotency.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_invariants.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_langchain.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_lifecycle.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_limits.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_packaging.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_paywall.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_paywall_state.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_redaction.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_webhook_events.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_webhook_timing.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/test_webhooks.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/__init__.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/test_async_cache.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/test_clock_skew_vs_replay_ttl.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/test_rotation.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/test_seen_cache_atomic.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/test_sign.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tests/webhooks/test_webhook_pipeline.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tools/api_baseline.json +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tools/api_diff.py +0 -0
- {solvapay_python-0.9.2 → solvapay_python-0.9.3}/tools/lint_invariants.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.3] — 2026-06-07
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Layer DAG now hard-enforced via single layered `import-linter` contract.** `tools/importlinter.cfg` replaces three sparse `forbidden` contracts with one `type = layers` contract covering the full HLD §V1.1 DAG (`_transport → _http → models → operations → client → paywall|webhooks → adapters → fastapi|langchain`). Closes the L2 `models` purity gap and the L5 `paywall`/`webhooks` framework-neutrality gap. `exhaustive = true` flags any new top-level module that ships without an explicit layer assignment. `ignore_type_checking_imports = true` strips `TYPE_CHECKING` blocks from the graph so cross-layer type hints do not trip the gate. CI-only change; no public-API impact.
|
|
8
|
+
|
|
9
|
+
|
|
3
10
|
## [0.9.2] — 2026-06-06
|
|
4
11
|
|
|
5
12
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solvapay-python
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
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
|
|
@@ -359,12 +359,15 @@ Default is `"2026-05-22"` (v0.9 ship date). Bump only on major SDK versions.
|
|
|
359
359
|
| 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 |
|
|
360
360
|
| 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 |
|
|
361
361
|
| v0.9.2 ✅ | Performance & stability quality — cold-import baseline harness, PEP 562 lazy adapter imports, `solvapay.sign_webhook` top-level re-export, microbenchmark harness (`solvapay[bench]`), troubleshooting docs |
|
|
362
|
+
| v0.9.3 ✅ | Layer DAG hard-enforcement — single `type = layers` `import-linter` contract replaces three sparse `forbidden` contracts; closes the L2 `models` purity gap and the L5 `paywall`/`webhooks` framework-neutrality gap; `exhaustive = true` flags any unlayered top-level module; `ignore_type_checking_imports = true` keeps cross-layer type hints lint-clean |
|
|
362
363
|
| v1.0 | Gated on founder signal — OpenAPI-generated models, WSGI/Lambda adapters, V2 planning |
|
|
363
364
|
|
|
364
365
|
---
|
|
365
366
|
|
|
366
367
|
## Status
|
|
367
368
|
|
|
369
|
+
**v0.9.3** — Layer DAG hard-enforcement quality patch. `tools/importlinter.cfg` now ships a single `type = layers` contract covering the full HLD §V1.1 DAG (`_transport → _http → models → operations → client → paywall|webhooks → adapters → fastapi|langchain`), replacing three sparse `forbidden` contracts. `exhaustive = true` flags any new top-level module that ships without an explicit layer assignment; `ignore_type_checking_imports = true` strips `TYPE_CHECKING` blocks from the dependency graph so cross-layer type hints do not trip the gate. New `tests/test_layer_dag.py` shells the contract from pytest as a belt-and-suspenders gate. `docs/architecture/layers.md` refreshed. CI-only change; no public-API impact. 306 tests. 87% line coverage. `mypy --strict` clean (45 files).
|
|
370
|
+
|
|
368
371
|
**v0.9.2** — Performance & stability quality patch. Cold-import baseline harness (`tests/test_cold_import.py`, 1.5x regression gate). PEP 562 lazy adapter imports — bare `import solvapay` no longer loads framework adapter modules. `solvapay.sign_webhook` promoted to top-level (alongside `verify_webhook`). Microbenchmark harness under `tests/benchmarks/` (opt-in `solvapay[bench]`). New docs: `docs/troubleshooting.md` (top-10 errors) and `docs/architecture/cold-start.md`. 305 tests. 87% line coverage. `mypy --strict` clean (45 files).
|
|
369
372
|
|
|
370
373
|
**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.
|
|
@@ -321,12 +321,15 @@ Default is `"2026-05-22"` (v0.9 ship date). Bump only on major SDK versions.
|
|
|
321
321
|
| 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 |
|
|
322
322
|
| 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 |
|
|
323
323
|
| v0.9.2 ✅ | Performance & stability quality — cold-import baseline harness, PEP 562 lazy adapter imports, `solvapay.sign_webhook` top-level re-export, microbenchmark harness (`solvapay[bench]`), troubleshooting docs |
|
|
324
|
+
| v0.9.3 ✅ | Layer DAG hard-enforcement — single `type = layers` `import-linter` contract replaces three sparse `forbidden` contracts; closes the L2 `models` purity gap and the L5 `paywall`/`webhooks` framework-neutrality gap; `exhaustive = true` flags any unlayered top-level module; `ignore_type_checking_imports = true` keeps cross-layer type hints lint-clean |
|
|
324
325
|
| v1.0 | Gated on founder signal — OpenAPI-generated models, WSGI/Lambda adapters, V2 planning |
|
|
325
326
|
|
|
326
327
|
---
|
|
327
328
|
|
|
328
329
|
## Status
|
|
329
330
|
|
|
331
|
+
**v0.9.3** — Layer DAG hard-enforcement quality patch. `tools/importlinter.cfg` now ships a single `type = layers` contract covering the full HLD §V1.1 DAG (`_transport → _http → models → operations → client → paywall|webhooks → adapters → fastapi|langchain`), replacing three sparse `forbidden` contracts. `exhaustive = true` flags any new top-level module that ships without an explicit layer assignment; `ignore_type_checking_imports = true` strips `TYPE_CHECKING` blocks from the dependency graph so cross-layer type hints do not trip the gate. New `tests/test_layer_dag.py` shells the contract from pytest as a belt-and-suspenders gate. `docs/architecture/layers.md` refreshed. CI-only change; no public-API impact. 306 tests. 87% line coverage. `mypy --strict` clean (45 files).
|
|
332
|
+
|
|
330
333
|
**v0.9.2** — Performance & stability quality patch. Cold-import baseline harness (`tests/test_cold_import.py`, 1.5x regression gate). PEP 562 lazy adapter imports — bare `import solvapay` no longer loads framework adapter modules. `solvapay.sign_webhook` promoted to top-level (alongside `verify_webhook`). Microbenchmark harness under `tests/benchmarks/` (opt-in `solvapay[bench]`). New docs: `docs/troubleshooting.md` (top-10 errors) and `docs/architecture/cold-start.md`. 305 tests. 87% line coverage. `mypy --strict` clean (45 files).
|
|
331
334
|
|
|
332
335
|
**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.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Layer Architecture (HLD V1.1)
|
|
2
|
+
|
|
3
|
+
The SDK is organized into strict layers. Each layer may only import from layers below it.
|
|
4
|
+
Violations fail CI via `import-linter` (see `tools/importlinter.cfg`).
|
|
5
|
+
|
|
6
|
+
From **v0.9.3** the DAG is enforced as a single `type = layers` contract, replacing the
|
|
7
|
+
earlier three sparse `forbidden` contracts. `exhaustive = true` flags any new top-level
|
|
8
|
+
module that ships without an explicit layer assignment.
|
|
9
|
+
|
|
10
|
+
## Layer DAG
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
L7 solvapay.fastapi — Top-level adapter shims (re-export from L6.adapters.*)
|
|
14
|
+
solvapay.langchain
|
|
15
|
+
L6 solvapay.adapters — Framework adapters (MCP, LangChain, ASGI, FastAPI)
|
|
16
|
+
L5 solvapay.paywall — Paywall / AsyncPaywall primitives
|
|
17
|
+
solvapay.webhooks — Webhook verify + pipeline
|
|
18
|
+
L4 solvapay.client — SolvaPay / AsyncSolvaPay public facades
|
|
19
|
+
solvapay._async_client
|
|
20
|
+
L3 solvapay.operations — Resource namespace operations (sv.customers.ensure etc.)
|
|
21
|
+
L2 solvapay.models — Pydantic request/response models
|
|
22
|
+
L1 solvapay._http — HTTP-client wrapper around the transport kernel
|
|
23
|
+
L0 solvapay._transport — Transport Protocol + httpx impl + middleware
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Note.** The HLD ships an aspirational L1 `_wire` slot that is currently unused.
|
|
27
|
+
> The shipped layout uses L1 for `_http` (the existing transport-kernel wrapper).
|
|
28
|
+
> When `_wire` ships, it will sit between `_transport` and `_http`.
|
|
29
|
+
|
|
30
|
+
## Allowed imports
|
|
31
|
+
|
|
32
|
+
A module at L_n may import from any module at L_(n-1) or below, plus the layer-independent
|
|
33
|
+
leaf modules (`exceptions`, `_stability`, `_config`, `idempotency`, `events`, `paywall_state`).
|
|
34
|
+
|
|
35
|
+
| Layer | May import |
|
|
36
|
+
|-------|------------|
|
|
37
|
+
| L0 `_transport` | stdlib, httpx, leaf modules |
|
|
38
|
+
| L1 `_http` | L0 + leaf modules |
|
|
39
|
+
| L2 `models` | stdlib, pydantic, leaf modules |
|
|
40
|
+
| L3 `operations` | L0–L2 + leaf modules |
|
|
41
|
+
| L4 `client` / `_async_client` | L0–L3 + leaf modules |
|
|
42
|
+
| L5 `paywall` / `webhooks` | L0–L4 + leaf modules |
|
|
43
|
+
| L6 `adapters` | L0–L5 + leaf modules |
|
|
44
|
+
| L7 `fastapi` / `langchain` (top-level shims) | L0–L6 + leaf modules |
|
|
45
|
+
| `tests/` | everything (permissive; tests live outside the `solvapay` container) |
|
|
46
|
+
|
|
47
|
+
Sibling modules on the same row (e.g. `paywall` ↔ `webhooks`, or `client` ↔ `_async_client`)
|
|
48
|
+
may not import each other. Each is independently constrained against the layers below.
|
|
49
|
+
|
|
50
|
+
## Why L7 = stability tier, not function (DG2 lock)
|
|
51
|
+
|
|
52
|
+
`solvapay.experimental` (reserved for v1.0) groups features by **stability**, not by
|
|
53
|
+
domain. A webhook adapter that is still unstable lives in L6 (adapters) while it's being
|
|
54
|
+
proven, then graduates to the canonical path once stable. This prevents the
|
|
55
|
+
`experimental` namespace from becoming a permanent home for half-finished features.
|
|
56
|
+
|
|
57
|
+
## Leaf modules (layer-independent)
|
|
58
|
+
|
|
59
|
+
These are listed in `exhaustive_ignores` in the contract because they have no upward edges
|
|
60
|
+
and no downward consumers beyond the boundary primitives:
|
|
61
|
+
|
|
62
|
+
- `solvapay.exceptions`
|
|
63
|
+
- `solvapay._stability`
|
|
64
|
+
- `solvapay._config`
|
|
65
|
+
- `solvapay.idempotency`
|
|
66
|
+
- `solvapay.events`
|
|
67
|
+
- `solvapay.paywall_state`
|
|
68
|
+
|
|
69
|
+
## TYPE_CHECKING imports
|
|
70
|
+
|
|
71
|
+
The contract sets `ignore_type_checking_imports = true`. Imports inside `if TYPE_CHECKING:`
|
|
72
|
+
blocks are stripped from the dependency graph, so cross-layer type hints (e.g.
|
|
73
|
+
`from solvapay.client import SolvaPay` for annotation purposes) do not trip the gate.
|
|
74
|
+
`mypy` still resolves these via the standard `from __future__ import annotations`
|
|
75
|
+
pattern enforced by `tools/lint_invariants.py`.
|
|
76
|
+
|
|
77
|
+
## Enforcement
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
~/.local/bin/uv run lint-imports --config tools/importlinter.cfg
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Runs in CI on every push and PR. Also wrapped by `tests/test_layer_dag.py` so the
|
|
84
|
+
contract is exercised by `pytest` for local feedback.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"darwin": 99.82}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Cold-import baseline harness.
|
|
2
|
+
|
|
3
|
+
Measures ``python -X importtime -c "import solvapay"`` and compares against
|
|
4
|
+
a committed per-platform baseline.
|
|
5
|
+
|
|
6
|
+
Behaviour:
|
|
7
|
+
- **CI** (``CI=true`` env var set by GitHub Actions): always prints the
|
|
8
|
+
measurement; never fails. GitHub runners have no persistent baseline across
|
|
9
|
+
fresh checkouts, so asserting would produce false failures on every run.
|
|
10
|
+
- **Local**: writes a per-platform baseline on first run; subsequent runs assert
|
|
11
|
+
within 1.5x. Catches cold-import regressions introduced during development.
|
|
12
|
+
|
|
13
|
+
Platform keys: ``sys.platform`` (``darwin``, ``linux``, ``win32``).
|
|
14
|
+
The hard <200 ms CI gate is a v1.0 feature (HLD §V1.20); v0.9.2 measures only.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
BASELINE_PATH = Path(__file__).parent / "_baselines" / "cold_import.json"
|
|
27
|
+
BASELINE_PATH.parent.mkdir(exist_ok=True)
|
|
28
|
+
REGRESSION_FACTOR = 1.5
|
|
29
|
+
PLATFORM = sys.platform # e.g. "darwin", "linux", "win32"
|
|
30
|
+
IN_CI = os.environ.get("CI", "").lower() in ("true", "1")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _measure_import_ms() -> float:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
[sys.executable, "-X", "importtime", "-c", "import solvapay"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
)
|
|
39
|
+
# importtime writes to stderr.
|
|
40
|
+
# Each line: "import time: <self_us> | <cumulative_us> | <module>"
|
|
41
|
+
lines = [ln for ln in result.stderr.splitlines() if "import time:" in ln]
|
|
42
|
+
if not lines:
|
|
43
|
+
raise RuntimeError(f"No importtime output captured.\nstderr:\n{result.stderr}")
|
|
44
|
+
# The outermost module (solvapay) has the highest cumulative value.
|
|
45
|
+
cumulative_us = max(
|
|
46
|
+
int(m.group(1)) for ln in lines if (m := re.search(r"import time:\s+\d+\s+\|\s+(\d+)", ln))
|
|
47
|
+
)
|
|
48
|
+
return cumulative_us / 1000.0 # µs → ms
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_cold_import_baseline() -> None:
|
|
52
|
+
"""Measure cold-import time. Assert vs baseline locally; print-only in CI."""
|
|
53
|
+
current_ms = _measure_import_ms()
|
|
54
|
+
|
|
55
|
+
if IN_CI:
|
|
56
|
+
# CI has no persistent state; just report the number.
|
|
57
|
+
print(f"\n[cold-import] CI platform={PLATFORM} current={current_ms:.1f} ms (no assertion)")
|
|
58
|
+
assert current_ms < 2000, (
|
|
59
|
+
f"Cold-import took {current_ms:.0f} ms — suspiciously slow even for CI"
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Local: per-platform baseline regression gate.
|
|
64
|
+
baselines: dict[str, float] = {}
|
|
65
|
+
if BASELINE_PATH.exists():
|
|
66
|
+
raw = json.loads(BASELINE_PATH.read_text())
|
|
67
|
+
# Support legacy flat format {"cold_import_ms": X}.
|
|
68
|
+
if "cold_import_ms" in raw and not any(k in raw for k in ("darwin", "linux", "win32")):
|
|
69
|
+
baselines = {PLATFORM: raw["cold_import_ms"]}
|
|
70
|
+
else:
|
|
71
|
+
baselines = raw
|
|
72
|
+
|
|
73
|
+
if PLATFORM not in baselines:
|
|
74
|
+
assert current_ms < 2000, (
|
|
75
|
+
f"Cold-import took {current_ms:.0f} ms on first run — suspiciously slow"
|
|
76
|
+
)
|
|
77
|
+
baselines[PLATFORM] = round(current_ms, 2)
|
|
78
|
+
BASELINE_PATH.write_text(json.dumps(baselines, sort_keys=True))
|
|
79
|
+
print(f"\n[cold-import] {PLATFORM} baseline written: {current_ms:.1f} ms")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
baseline_ms: float = baselines[PLATFORM]
|
|
83
|
+
limit_ms = baseline_ms * REGRESSION_FACTOR
|
|
84
|
+
|
|
85
|
+
print(
|
|
86
|
+
f"\n[cold-import] platform={PLATFORM} current={current_ms:.1f} ms "
|
|
87
|
+
f"baseline={baseline_ms:.1f} ms limit={limit_ms:.1f} ms"
|
|
88
|
+
)
|
|
89
|
+
assert current_ms <= limit_ms, (
|
|
90
|
+
f"Cold-import regression on {PLATFORM}: {current_ms:.1f} ms > {limit_ms:.1f} ms "
|
|
91
|
+
f"(baseline={baseline_ms:.1f} ms x {REGRESSION_FACTOR})"
|
|
92
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Belt-and-suspenders enforcement of the HLD §V1.1 layer DAG.
|
|
2
|
+
|
|
3
|
+
The primary gate is the CI step that runs ``lint-imports`` directly. This
|
|
4
|
+
test shells out to the same command so that ``pytest`` also fails locally
|
|
5
|
+
if someone introduces an upward import without re-running ``lint-imports``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
CONFIG = REPO_ROOT / "tools" / "importlinter.cfg"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_layered_dag_contract_holds() -> None:
|
|
21
|
+
lint_imports = shutil.which("lint-imports")
|
|
22
|
+
if lint_imports is None:
|
|
23
|
+
pytest.skip("lint-imports not on PATH; CI runs it directly")
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
[lint_imports, "--config", str(CONFIG)],
|
|
26
|
+
cwd=REPO_ROOT,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
check=False,
|
|
30
|
+
)
|
|
31
|
+
assert result.returncode == 0, (
|
|
32
|
+
f"lint-imports failed (rc={result.returncode}).\n"
|
|
33
|
+
f"stdout:\n{result.stdout}\n"
|
|
34
|
+
f"stderr:\n{result.stderr}"
|
|
35
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[importlinter]
|
|
2
|
+
root_package = solvapay
|
|
3
|
+
include_external_packages = False
|
|
4
|
+
# TYPE_CHECKING blocks vanish from the import graph; cross-layer type hints
|
|
5
|
+
# do not trip the layered DAG check below. mypy still sees them via the
|
|
6
|
+
# `from __future__ import annotations` + `TYPE_CHECKING:` pattern.
|
|
7
|
+
ignore_type_checking_imports = True
|
|
8
|
+
|
|
9
|
+
[importlinter:contract:layered-dag]
|
|
10
|
+
name = solvapay layer DAG (HLD V1.1)
|
|
11
|
+
type = layers
|
|
12
|
+
exhaustive = true
|
|
13
|
+
exhaustive_ignores =
|
|
14
|
+
exceptions
|
|
15
|
+
_stability
|
|
16
|
+
_config
|
|
17
|
+
idempotency
|
|
18
|
+
events
|
|
19
|
+
paywall_state
|
|
20
|
+
layers =
|
|
21
|
+
fastapi | langchain
|
|
22
|
+
adapters
|
|
23
|
+
paywall | webhooks
|
|
24
|
+
client | _async_client
|
|
25
|
+
operations
|
|
26
|
+
models
|
|
27
|
+
_http
|
|
28
|
+
_transport
|
|
29
|
+
containers =
|
|
30
|
+
solvapay
|
|
31
|
+
ignore_imports =
|
|
32
|
+
solvapay.paywall.state -> solvapay.paywall_state
|
|
@@ -2003,6 +2003,15 @@ wheels = [
|
|
|
2003
2003
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
2004
2004
|
]
|
|
2005
2005
|
|
|
2006
|
+
[[package]]
|
|
2007
|
+
name = "py-cpuinfo"
|
|
2008
|
+
version = "9.0.0"
|
|
2009
|
+
source = { registry = "https://pypi.org/simple" }
|
|
2010
|
+
sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" }
|
|
2011
|
+
wheels = [
|
|
2012
|
+
{ url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" },
|
|
2013
|
+
]
|
|
2014
|
+
|
|
2006
2015
|
[[package]]
|
|
2007
2016
|
name = "py-key-value-aio"
|
|
2008
2017
|
version = "0.4.4"
|
|
@@ -2291,6 +2300,19 @@ wheels = [
|
|
|
2291
2300
|
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
|
2292
2301
|
]
|
|
2293
2302
|
|
|
2303
|
+
[[package]]
|
|
2304
|
+
name = "pytest-benchmark"
|
|
2305
|
+
version = "5.2.3"
|
|
2306
|
+
source = { registry = "https://pypi.org/simple" }
|
|
2307
|
+
dependencies = [
|
|
2308
|
+
{ name = "py-cpuinfo" },
|
|
2309
|
+
{ name = "pytest" },
|
|
2310
|
+
]
|
|
2311
|
+
sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" }
|
|
2312
|
+
wheels = [
|
|
2313
|
+
{ url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" },
|
|
2314
|
+
]
|
|
2315
|
+
|
|
2294
2316
|
[[package]]
|
|
2295
2317
|
name = "pytest-cov"
|
|
2296
2318
|
version = "7.1.0"
|
|
@@ -2692,7 +2714,7 @@ wheels = [
|
|
|
2692
2714
|
|
|
2693
2715
|
[[package]]
|
|
2694
2716
|
name = "solvapay-python"
|
|
2695
|
-
version = "0.9.
|
|
2717
|
+
version = "0.9.3"
|
|
2696
2718
|
source = { editable = "." }
|
|
2697
2719
|
dependencies = [
|
|
2698
2720
|
{ name = "httpx" },
|
|
@@ -2700,6 +2722,9 @@ dependencies = [
|
|
|
2700
2722
|
]
|
|
2701
2723
|
|
|
2702
2724
|
[package.optional-dependencies]
|
|
2725
|
+
bench = [
|
|
2726
|
+
{ name = "pytest-benchmark" },
|
|
2727
|
+
]
|
|
2703
2728
|
fastapi = [
|
|
2704
2729
|
{ name = "fastapi" },
|
|
2705
2730
|
]
|
|
@@ -2742,9 +2767,10 @@ requires-dist = [
|
|
|
2742
2767
|
{ name = "httpx", specifier = ">=0.27,<0.29" },
|
|
2743
2768
|
{ name = "langchain-core", marker = "extra == 'langchain'", specifier = ">=0.3,<1.5" },
|
|
2744
2769
|
{ name = "pydantic", specifier = ">=2.6,<2.14" },
|
|
2770
|
+
{ name = "pytest-benchmark", marker = "extra == 'bench'", specifier = ">=4.0,<6" },
|
|
2745
2771
|
{ name = "tenacity", marker = "extra == 'retry'", specifier = ">=8.2,<10" },
|
|
2746
2772
|
]
|
|
2747
|
-
provides-extras = ["fastapi", "langchain", "mcp", "asgi", "retry"]
|
|
2773
|
+
provides-extras = ["fastapi", "langchain", "mcp", "asgi", "retry", "bench"]
|
|
2748
2774
|
|
|
2749
2775
|
[package.metadata.requires-dev]
|
|
2750
2776
|
dev = [
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Layer Architecture (HLD V1.1)
|
|
2
|
-
|
|
3
|
-
The SDK is organized into strict layers. Each layer may only import from layers below it.
|
|
4
|
-
Violations fail CI via `import-linter` (see `tools/importlinter.cfg`).
|
|
5
|
-
|
|
6
|
-
## Layer DAG
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
L0 solvapay._transport — Transport Protocol + httpx impl + middleware
|
|
10
|
-
L1 solvapay._wire — Wire codec (reserved; currently unused)
|
|
11
|
-
L2 solvapay.models — Pydantic request/response models
|
|
12
|
-
L3 solvapay.operations — Resource namespace operations (sv.customers.ensure etc.)
|
|
13
|
-
L4 solvapay.client — SolvaPay / AsyncSolvaPay public facades
|
|
14
|
-
solvapay._async_client
|
|
15
|
-
L5 solvapay.paywall — Paywall / AsyncPaywall primitives
|
|
16
|
-
solvapay.webhooks — Webhook verify + pipeline
|
|
17
|
-
L6 solvapay.adapters — Framework adapters (MCP, LangChain, ASGI)
|
|
18
|
-
L7 solvapay.experimental — Experimental features (reserved)
|
|
19
|
-
L8 tests/ — Test suite (permissive; may import L0–L7)
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Allowed imports
|
|
23
|
-
|
|
24
|
-
| Layer | May import |
|
|
25
|
-
|-------|-----------|
|
|
26
|
-
| L0 `_transport` | stdlib, httpx, `exceptions` |
|
|
27
|
-
| L1 `_wire` | L0 |
|
|
28
|
-
| L2 `models` | stdlib, pydantic |
|
|
29
|
-
| L3 `operations` | L0, L1, L2 |
|
|
30
|
-
| L4 `client` | L0–L3, `_config`, `exceptions` |
|
|
31
|
-
| L5 `paywall` | L0–L4 |
|
|
32
|
-
| L5 `webhooks` | L0–L2, `exceptions` |
|
|
33
|
-
| L6 `adapters` | L0–L5 |
|
|
34
|
-
| L7 `experimental` | L0–L6 |
|
|
35
|
-
| L8 `tests` | L0–L7 (no `from solvapay._*` outside `tests/internal/`) |
|
|
36
|
-
|
|
37
|
-
## Why L7 = stability tier, not function (DG2 lock)
|
|
38
|
-
|
|
39
|
-
`solvapay.experimental` groups features by **stability**, not by domain. A webhook adapter
|
|
40
|
-
that is still unstable lives in L6 (adapters) while it's being proven, then graduates to
|
|
41
|
-
the canonical path once stable. This prevents the "experimental" namespace from becoming a
|
|
42
|
-
permanent home for half-finished features.
|
|
43
|
-
|
|
44
|
-
## Enforcement
|
|
45
|
-
|
|
46
|
-
`uv run lint-imports --config tools/importlinter.cfg` — run in CI on every push.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"cold_import_ms": 99.82}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"""Cold-import baseline harness.
|
|
2
|
-
|
|
3
|
-
Runs ``python -X importtime -c "import solvapay"`` via subprocess, parses the
|
|
4
|
-
highest cumulative time from the importtime output, and writes a baseline on
|
|
5
|
-
first run. Subsequent runs assert the current time is within 1.5x the baseline.
|
|
6
|
-
|
|
7
|
-
CI prints the absolute ms value so regressions are visible in the test name.
|
|
8
|
-
The hard <200 ms gate is a v1.0 feature (HLD §V1.20); this only measures.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import re
|
|
15
|
-
import subprocess
|
|
16
|
-
import sys
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
BASELINE_PATH = Path(__file__).parent / "_baselines" / "cold_import.json"
|
|
20
|
-
BASELINE_PATH.parent.mkdir(exist_ok=True)
|
|
21
|
-
REGRESSION_FACTOR = 1.5
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _measure_import_ms() -> float:
|
|
25
|
-
result = subprocess.run(
|
|
26
|
-
[sys.executable, "-X", "importtime", "-c", "import solvapay"],
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
)
|
|
30
|
-
# importtime writes to stderr.
|
|
31
|
-
# Each line: "import time: <self_us> | <cumulative_us> | <module>"
|
|
32
|
-
lines = [ln for ln in result.stderr.splitlines() if "import time:" in ln]
|
|
33
|
-
if not lines:
|
|
34
|
-
raise RuntimeError(f"No importtime output captured.\nstderr:\n{result.stderr}")
|
|
35
|
-
# The outermost module (solvapay) has the highest cumulative value.
|
|
36
|
-
cumulative_us = max(
|
|
37
|
-
int(m.group(1)) for ln in lines if (m := re.search(r"import time:\s+\d+\s+\|\s+(\d+)", ln))
|
|
38
|
-
)
|
|
39
|
-
return cumulative_us / 1000.0 # µs → ms
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_cold_import_baseline() -> None:
|
|
43
|
-
"""Measure cold-import time and assert within 1.5x of committed baseline."""
|
|
44
|
-
current_ms = _measure_import_ms()
|
|
45
|
-
|
|
46
|
-
if not BASELINE_PATH.exists():
|
|
47
|
-
# Sanity: import must complete in <2 s even on first run (no regression).
|
|
48
|
-
assert current_ms < 2000, (
|
|
49
|
-
f"Cold-import took {current_ms:.0f} ms on first run — suspiciously slow"
|
|
50
|
-
)
|
|
51
|
-
BASELINE_PATH.write_text(json.dumps({"cold_import_ms": round(current_ms, 2)}))
|
|
52
|
-
print(f"\n[cold-import] Baseline written: {current_ms:.1f} ms")
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
baseline = json.loads(BASELINE_PATH.read_text())
|
|
56
|
-
baseline_ms: float = baseline["cold_import_ms"]
|
|
57
|
-
limit_ms = baseline_ms * REGRESSION_FACTOR
|
|
58
|
-
|
|
59
|
-
print(
|
|
60
|
-
f"\n[cold-import] current={current_ms:.1f} ms "
|
|
61
|
-
f"baseline={baseline_ms:.1f} ms limit={limit_ms:.1f} ms"
|
|
62
|
-
)
|
|
63
|
-
assert current_ms <= limit_ms, (
|
|
64
|
-
f"Cold-import regression: {current_ms:.1f} ms > {limit_ms:.1f} ms "
|
|
65
|
-
f"(baseline={baseline_ms:.1f} ms x {REGRESSION_FACTOR})"
|
|
66
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
[importlinter]
|
|
2
|
-
root_packages =
|
|
3
|
-
solvapay
|
|
4
|
-
include_external_packages = True
|
|
5
|
-
|
|
6
|
-
[importlinter:contract:no-upward-imports]
|
|
7
|
-
name = Layer DAG — no upward imports (HLD V1.1)
|
|
8
|
-
type = forbidden
|
|
9
|
-
|
|
10
|
-
# L0: _transport must not import higher layers
|
|
11
|
-
source_modules =
|
|
12
|
-
solvapay._transport
|
|
13
|
-
forbidden_modules =
|
|
14
|
-
solvapay.client
|
|
15
|
-
solvapay._async_client
|
|
16
|
-
solvapay.paywall
|
|
17
|
-
solvapay.webhooks
|
|
18
|
-
solvapay.adapters
|
|
19
|
-
solvapay.operations
|
|
20
|
-
|
|
21
|
-
[importlinter:contract:no-client-importing-adapters]
|
|
22
|
-
name = Layer DAG — client (L4) must not import adapters (L6) or experimental (L7)
|
|
23
|
-
type = forbidden
|
|
24
|
-
|
|
25
|
-
source_modules =
|
|
26
|
-
solvapay.client
|
|
27
|
-
solvapay._async_client
|
|
28
|
-
|
|
29
|
-
forbidden_modules =
|
|
30
|
-
solvapay.adapters
|
|
31
|
-
solvapay.experimental
|
|
32
|
-
|
|
33
|
-
[importlinter:contract:operations-no-paywall]
|
|
34
|
-
name = Layer DAG — operations (L3) must not import paywall (L5) or adapters (L6)
|
|
35
|
-
type = forbidden
|
|
36
|
-
|
|
37
|
-
source_modules =
|
|
38
|
-
solvapay.operations
|
|
39
|
-
|
|
40
|
-
forbidden_modules =
|
|
41
|
-
solvapay.paywall
|
|
42
|
-
solvapay.adapters
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|