imperal-sdk 5.2.2__tar.gz → 5.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/CHANGELOG.md +30 -0
  2. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/PKG-INFO +1 -1
  3. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/api_surface.json +7 -0
  4. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/sdk_claims.json +12 -1
  5. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/__init__.py +1 -1
  6. imperal_sdk-5.4.0/src/imperal_sdk/billing/client.py +325 -0
  7. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/context.py +13 -0
  8. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/devtools/generate_sdk_claims.py +3 -0
  9. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/models.py +76 -0
  10. imperal_sdk-5.4.0/tests/contract/test_artifacts_freshness.py +101 -0
  11. imperal_sdk-5.4.0/tests/fixtures/contract/kernel-contract.sample.json +202 -0
  12. imperal_sdk-5.4.0/tests/test_billing.py +199 -0
  13. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_import_light.py +5 -1
  14. imperal_sdk-5.2.2/src/imperal_sdk/billing/client.py +0 -162
  15. imperal_sdk-5.2.2/tests/fixtures/contract/kernel-contract.sample.json +0 -28
  16. imperal_sdk-5.2.2/tests/test_billing.py +0 -33
  17. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/.github/workflows/identity-contract.yml +0 -0
  18. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/.github/workflows/publish.yml +0 -0
  19. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/.github/workflows/test.yml +0 -0
  20. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/.gitignore +0 -0
  21. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/LICENSE +0 -0
  22. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/README.md +0 -0
  23. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/docs/sdl-facets.md +0 -0
  24. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/pyproject.toml +0 -0
  25. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/sdl_roles.json +0 -0
  26. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/.codebase-index-cache.pkl +0 -0
  27. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ai/__init__.py +0 -0
  28. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ai/client.py +0 -0
  29. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/auth/__init__.py +0 -0
  30. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/auth/client.py +0 -0
  31. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/auth/middleware.py +0 -0
  32. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/billing/__init__.py +0 -0
  33. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/cache/__init__.py +0 -0
  34. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/cache/client.py +0 -0
  35. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/cache/protocol.py +0 -0
  36. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/__init__.py +0 -0
  37. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/action_result.py +0 -0
  38. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/error_codes.py +0 -0
  39. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/exceptions.py +0 -0
  40. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/extension.py +0 -0
  41. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/filters.py +0 -0
  42. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/guards.py +0 -0
  43. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/kernel_primitives.py +0 -0
  44. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/narration.py +0 -0
  45. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/narration_guard.py +0 -0
  46. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/prompt.py +0 -0
  47. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/chat/refusal.py +0 -0
  48. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/cli/__init__.py +0 -0
  49. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/cli/main.py +0 -0
  50. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/config/__init__.py +0 -0
  51. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/config/client.py +0 -0
  52. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/devtools/__init__.py +0 -0
  53. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/devtools/contract_checks.py +0 -0
  54. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/devtools/generate_api_surface.py +0 -0
  55. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/devtools/validate_identity_contract.py +0 -0
  56. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/errors.py +0 -0
  57. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/extension.py +0 -0
  58. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/extensions/__init__.py +0 -0
  59. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/extensions/client.py +0 -0
  60. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/http/__init__.py +0 -0
  61. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/http/client.py +0 -0
  62. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/manifest.py +0 -0
  63. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/manifest_schema.py +0 -0
  64. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/notify/__init__.py +0 -0
  65. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/notify/client.py +0 -0
  66. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/prompts/__init__.py +0 -0
  67. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/prompts/icnli_integrity_rules.txt +0 -0
  68. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/prompts/kernel_formatting_rule.txt +0 -0
  69. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/prompts/kernel_proactivity_rule.txt +0 -0
  70. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/protocols.py +0 -0
  71. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/rpc/__init__.py +0 -0
  72. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/rpc/codec.py +0 -0
  73. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/rpc/contract.py +0 -0
  74. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/runtime/__init__.py +0 -0
  75. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/runtime/executor.py +0 -0
  76. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/runtime/llm_provider.py +0 -0
  77. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/runtime/message_adapter.py +0 -0
  78. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/action_result.schema.json +0 -0
  79. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/balance_info.schema.json +0 -0
  80. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/chat_result.schema.json +0 -0
  81. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/completion_result.schema.json +0 -0
  82. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/document.schema.json +0 -0
  83. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/event.schema.json +0 -0
  84. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/file_info.schema.json +0 -0
  85. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/function_call.schema.json +0 -0
  86. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/http_response.schema.json +0 -0
  87. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/imperal.schema.json +0 -0
  88. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/limits_result.schema.json +0 -0
  89. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/schemas/subscription_info.schema.json +0 -0
  90. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/__init__.py +0 -0
  91. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/_generate_roles_json.py +0 -0
  92. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/entity.py +0 -0
  93. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/__init__.py +0 -0
  94. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/catalog.py +0 -0
  95. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/comm.py +0 -0
  96. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/content.py +0 -0
  97. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/device.py +0 -0
  98. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/event.py +0 -0
  99. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/geo.py +0 -0
  100. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/identity.py +0 -0
  101. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/media.py +0 -0
  102. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/metric.py +0 -0
  103. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/money.py +0 -0
  104. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/net.py +0 -0
  105. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/people.py +0 -0
  106. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/quantity.py +0 -0
  107. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/rating.py +0 -0
  108. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/security.py +0 -0
  109. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/task.py +0 -0
  110. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/facets/time.py +0 -0
  111. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/field.py +0 -0
  112. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/sdl/roles.py +0 -0
  113. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/secrets/__init__.py +0 -0
  114. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/secrets/client.py +0 -0
  115. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/secrets/exceptions.py +0 -0
  116. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/secrets/panel_handler.py +0 -0
  117. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/secrets/spec.py +0 -0
  118. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/security/__init__.py +0 -0
  119. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/security/call_token.py +0 -0
  120. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/skeleton/__init__.py +0 -0
  121. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/skeleton/client.py +0 -0
  122. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/storage/__init__.py +0 -0
  123. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/storage/client.py +0 -0
  124. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/store/__init__.py +0 -0
  125. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/store/client.py +0 -0
  126. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/store/exceptions.py +0 -0
  127. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/testing/__init__.py +0 -0
  128. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/testing/mock_context.py +0 -0
  129. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/testing/mock_secrets.py +0 -0
  130. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/__init__.py +0 -0
  131. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/action_result.py +0 -0
  132. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/chat_result.py +0 -0
  133. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/client_contracts.py +0 -0
  134. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/contracts.py +0 -0
  135. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/contributions.py +0 -0
  136. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/events.py +0 -0
  137. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/health.py +0 -0
  138. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/identity.py +0 -0
  139. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/pagination.py +0 -0
  140. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/types/store_contracts.py +0 -0
  141. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/__init__.py +0 -0
  142. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/actions.py +0 -0
  143. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/base.py +0 -0
  144. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/data.py +0 -0
  145. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/display.py +0 -0
  146. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/feedback.py +0 -0
  147. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/graph.py +0 -0
  148. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/input_components.py +0 -0
  149. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/interactive.py +0 -0
  150. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/layout.py +0 -0
  151. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/ui/theme.py +0 -0
  152. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/validator.py +0 -0
  153. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/src/imperal_sdk/validator_v1_6_0.py +0 -0
  154. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/conftest.py +0 -0
  155. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contract/__init__.py +0 -0
  156. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contract/test_contract_checks_selftest.py +0 -0
  157. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contract/test_generate_sdk_claims.py +0 -0
  158. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contract/test_sample_contract_shape.py +0 -0
  159. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contract/test_sdk_matches_kernel_contract.py +0 -0
  160. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contracts/__init__.py +0 -0
  161. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/contracts/test_store_contracts.py +0 -0
  162. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/fixtures/openapi/auth-gateway.json +0 -0
  163. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/fixtures/openapi/registry.json +0 -0
  164. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/fixtures/openapi/sharelock-cases.json +0 -0
  165. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/rpc/__init__.py +0 -0
  166. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/rpc/test_codec.py +0 -0
  167. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/rpc/test_contract.py +0 -0
  168. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/runtime/__init__.py +0 -0
  169. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/runtime/test_llm_provider_config_store.py +0 -0
  170. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/runtime/test_llm_provider_ctx_injection.py +0 -0
  171. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/store/__init__.py +0 -0
  172. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/store/test_list_users_client.py +0 -0
  173. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/store/test_query_all_client.py +0 -0
  174. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_action_result_typed.py +0 -0
  175. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_as_user.py +0 -0
  176. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_auth.py +0 -0
  177. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_cache_client.py +0 -0
  178. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_cache_model.py +0 -0
  179. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_call_token.py +0 -0
  180. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_extension_deprecation.py +0 -0
  181. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_extension_no_llm_router.py +0 -0
  182. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_filters.py +0 -0
  183. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_function_background_flag.py +0 -0
  184. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_guards.py +0 -0
  185. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_guards_bleed.py +0 -0
  186. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_prompt.py +0 -0
  187. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_chat_result.py +0 -0
  188. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_cli.py +0 -0
  189. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_client_contracts.py +0 -0
  190. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_config_client.py +0 -0
  191. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_context.py +0 -0
  192. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_context_background_task.py +0 -0
  193. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_context_deliver_chat_message.py +0 -0
  194. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_context_guards.py +0 -0
  195. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_contracts.py +0 -0
  196. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_contracts_live.py +0 -0
  197. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_contributions.py +0 -0
  198. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_data_model_kwarg.py +0 -0
  199. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_document_contract.py +0 -0
  200. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_emits_decorator.py +0 -0
  201. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_error_codes.py +0 -0
  202. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_errors.py +0 -0
  203. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_event_schema_v2.py +0 -0
  204. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_events_health.py +0 -0
  205. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_extension.py +0 -0
  206. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_extension_v2.py +0 -0
  207. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_extensions_emit.py +0 -0
  208. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_http_timeout_override.py +0 -0
  209. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_id_shape_guard.py +0 -0
  210. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_identity_contract.py +0 -0
  211. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_imperal_schema_v2.py +0 -0
  212. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_kernel_primitives.py +0 -0
  213. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest.py +0 -0
  214. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_no_orchestrator_tool.py +0 -0
  215. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_roundtrip_gate.py +0 -0
  216. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_schema.py +0 -0
  217. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_v2_events.py +0 -0
  218. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_v2_other_sections.py +0 -0
  219. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_v2_webhooks.py +0 -0
  220. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_manifest_validator_v2.py +0 -0
  221. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_mock_context.py +0 -0
  222. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_models.py +0 -0
  223. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_narration_emission.py +0 -0
  224. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_narration_guard.py +0 -0
  225. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_openai_max_completion_tokens.py +0 -0
  226. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_pagination.py +0 -0
  227. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_panel_rendering_contract.py +0 -0
  228. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_panels.py +0 -0
  229. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_phase_a_dead_removal.py +0 -0
  230. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_phase_a_drift.py +0 -0
  231. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_phase_a_text.py +0 -0
  232. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdk_version_stamp.py +0 -0
  233. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_entity.py +0 -0
  234. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_entity_marker.py +0 -0
  235. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_exports.py +0 -0
  236. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_catalog.py +0 -0
  237. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_collisions.py +0 -0
  238. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_comm.py +0 -0
  239. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_content.py +0 -0
  240. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_device.py +0 -0
  241. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_event.py +0 -0
  242. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_exports.py +0 -0
  243. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_field.py +0 -0
  244. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_geo.py +0 -0
  245. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_identity.py +0 -0
  246. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_media.py +0 -0
  247. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_metric.py +0 -0
  248. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_money.py +0 -0
  249. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_net.py +0 -0
  250. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_people.py +0 -0
  251. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_quantity.py +0 -0
  252. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_rating.py +0 -0
  253. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_security.py +0 -0
  254. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_task.py +0 -0
  255. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facet_time.py +0 -0
  256. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facets_catalog.py +0 -0
  257. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facets_doc.py +0 -0
  258. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_facets_pkg.py +0 -0
  259. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_field.py +0 -0
  260. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_roles.py +0 -0
  261. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_roles_json.py +0 -0
  262. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_sdl_roles_of_facets.py +0 -0
  263. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_skeleton_decorator.py +0 -0
  264. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_spec_validation.py +0 -0
  265. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_ui.py +0 -0
  266. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_ui_fileupload_enhanced.py +0 -0
  267. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_ui_html.py +0 -0
  268. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_ui_image_enhanced.py +0 -0
  269. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_ui_open.py +0 -0
  270. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_ui_theme.py +0 -0
  271. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_user.py +0 -0
  272. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_v7_emit_refusal.py +0 -0
  273. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_validator.py +0 -0
  274. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_validator_drift.py +0 -0
  275. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_validator_pep563.py +0 -0
  276. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_validator_v1_6_0_rules.py +0 -0
  277. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_validator_v25.py +0 -0
  278. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/test_write_arg_bleed.py +0 -0
  279. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/tools/__init__.py +0 -0
  280. {imperal_sdk-5.2.2 → imperal_sdk-5.4.0}/tests/tools/test_generate_api_surface.py +0 -0
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to `imperal-sdk` are documented here.
4
4
 
5
+ ## 5.4.0 — 2026-06-16 — BillingClient portal + full Webbee parity
6
+
7
+ Additive — **nothing to migrate**.
8
+
9
+ ### Added
10
+ - `ctx.billing.create_billing_portal_session()` — mints a Stripe Customer
11
+ Portal session and returns its hosted URL (for `ui.Open`), so extensions can
12
+ let users manage cards + view invoices on Stripe's hosted page (PAN never
13
+ touches our backend). Surfaces errors.
14
+ - Five `ctx.billing` parity methods so Webbee can fully drive billing via chat:
15
+ `list_plans()` (public plan catalog → `list[PlanInfo]`, safe-degrades to `[]`),
16
+ `get_auto_topup()` (→ `AutoTopupSettings`, safe-degrades to disabled defaults),
17
+ `set_auto_topup(enabled, threshold_pct=10, recharge_tokens=20000, payment_method_id="")`
18
+ (surfaces errors), `cancel_subscription()` (cancel-at-period-end → result dict,
19
+ surfaces errors), `update_billing_profile(profile)` (writes name/company/vat/country,
20
+ surfaces errors).
21
+ - New dataclasses `PlanInfo` and `AutoTopupSettings` in `imperal_sdk.types.models`.
22
+
23
+ ## 5.3.0 — 2026-06-16 — BillingClient write/payment methods
24
+
25
+ Additive — **nothing to migrate**.
26
+
27
+ ### Added
28
+ - `ctx.billing` write/payment methods: `list_payment_methods`, `list_payments`,
29
+ `create_setup_intent`, `set_default_payment_method`, `remove_payment_method`,
30
+ `change_plan`, `topup`. Reads degrade safely; writes surface errors so the
31
+ caller can render Stripe failures / drive the Payment Element.
32
+ - `BillingClient` now sends `X-Acting-User` on the service-token path so
33
+ `get_user_or_service` gateway endpoints resolve the acting user.
34
+
5
35
  ## 5.2.2 — 2026-06-11 — Import-light package root
6
36
 
7
37
  Performance / robustness release. **Zero API changes** — every public name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imperal-sdk
3
- Version: 5.2.2
3
+ Version: 5.4.0
4
4
  Summary: SDK for building Imperal Cloud extensions
5
5
  Author: Valentin Scerbacov, Imperal, Inc.
6
6
  License-Expression: AGPL-3.0-or-later
@@ -3,9 +3,16 @@
3
3
  "complete"
4
4
  ],
5
5
  "billing": [
6
+ "change_plan",
6
7
  "check_limits",
8
+ "create_setup_intent",
7
9
  "get_balance",
8
10
  "get_subscription",
11
+ "list_payment_methods",
12
+ "list_payments",
13
+ "remove_payment_method",
14
+ "set_default_payment_method",
15
+ "topup",
9
16
  "track_usage"
10
17
  ],
11
18
  "config": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "_sdk_version": "5.1.0",
2
+ "_sdk_version": "5.4.0",
3
3
  "constants": {
4
4
  "max_call_depth": {
5
5
  "counts_root": true,
@@ -17,11 +17,22 @@
17
17
  "long_running": "advisory"
18
18
  },
19
19
  "http_payloads": {
20
+ "POST /v1/billing/change-plan": [
21
+ "plan_id",
22
+ "period"
23
+ ],
20
24
  "POST /v1/billing/internal/usage/track": [
21
25
  "meter",
22
26
  "quantity",
23
27
  "user_id",
24
28
  "tenant_id"
29
+ ],
30
+ "POST /v1/billing/payment-methods/setup": [],
31
+ "POST /v1/billing/topup": [
32
+ "tokens",
33
+ "price_cents",
34
+ "save_payment_method",
35
+ "off_session"
25
36
  ]
26
37
  }
27
38
  }
@@ -1,7 +1,7 @@
1
1
  """Imperal Cloud SDK — build extensions for the Imperal platform."""
2
2
  from typing import TYPE_CHECKING
3
3
 
4
- __version__ = "5.2.2"
4
+ __version__ = "5.4.0"
5
5
 
6
6
  # 5.2.2 (2026-06-11): the package root resolves its public surface lazily
7
7
  # (PEP 562). The eager imports pulled the HTTP transport (Context / client
@@ -0,0 +1,325 @@
1
+ # Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
2
+ # Licensed under the AGPL-3.0 License. See LICENSE file for details.
3
+ from __future__ import annotations
4
+ import logging
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+ import httpx
8
+
9
+ from imperal_sdk.types.models import (
10
+ BalanceInfo, PaymentMethod, SetupIntentResult, ChangePlanResult,
11
+ TopupResult, PaymentRecord, PlanInfo, AutoTopupSettings,
12
+ )
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class LimitsResult:
19
+ plan: str
20
+ usage: dict[str, int]
21
+ limits: dict[str, int]
22
+ exceeded: list[str]
23
+
24
+ def is_exceeded(self, meter: str) -> bool:
25
+ return meter in self.exceeded
26
+
27
+ @property
28
+ def any_exceeded(self) -> bool:
29
+ return bool(self.exceeded)
30
+
31
+
32
+ @dataclass
33
+ class SubscriptionInfo:
34
+ plan: str
35
+ status: str
36
+ started_at: str | None = None
37
+ expires_at: str | None = None
38
+
39
+
40
+ class BillingClient:
41
+ """Read-only billing client for extensions.
42
+
43
+ When initialized with service_token (kernel context), uses internal
44
+ endpoints with X-Service-Token header. When initialized with auth_token
45
+ (user JWT from Panel), uses public endpoints with Authorization header.
46
+ """
47
+
48
+ def __init__(
49
+ self, gateway_url: str, auth_token: str = "",
50
+ service_token: str = "", user_id: str = "",
51
+ ):
52
+ self._gateway_url = gateway_url.rstrip("/")
53
+ self._service_token = service_token
54
+ self._auth_token = auth_token
55
+ self._user_id = user_id
56
+
57
+ def _headers(self) -> dict:
58
+ if self._service_token:
59
+ h = {"X-Service-Token": self._service_token}
60
+ if self._user_id:
61
+ h["X-Acting-User"] = self._user_id
62
+ return h
63
+ return {"Authorization": f"Bearer {self._auth_token}"}
64
+
65
+ def _uid(self, user: Any = None) -> str:
66
+ return self._user_id or (getattr(user, "id", "") if user else "")
67
+
68
+ async def check_limits(self, user: Any = None) -> LimitsResult:
69
+ uid = self._uid(user)
70
+ try:
71
+ async with httpx.AsyncClient() as client:
72
+ if self._service_token and uid:
73
+ resp = await client.get(
74
+ f"{self._gateway_url}/v1/billing/internal/user-limits/{uid}",
75
+ headers=self._headers(), timeout=10)
76
+ else:
77
+ resp = await client.get(
78
+ f"{self._gateway_url}/v1/billing/usage",
79
+ headers=self._headers(), timeout=10)
80
+ resp.raise_for_status()
81
+ data = resp.json()
82
+ return LimitsResult(
83
+ plan=data.get("plan", "free"),
84
+ usage=data.get("usage", {}),
85
+ limits=data.get("limits", {}),
86
+ exceeded=data.get("exceeded", []),
87
+ )
88
+ except Exception as e:
89
+ log.warning("Billing check_limits failed: %s", e)
90
+ return LimitsResult(plan="unknown", usage={}, limits={}, exceeded=[])
91
+
92
+ async def get_subscription(self, user: Any = None) -> SubscriptionInfo:
93
+ uid = self._uid(user)
94
+ try:
95
+ async with httpx.AsyncClient() as client:
96
+ if self._service_token and uid:
97
+ resp = await client.get(
98
+ f"{self._gateway_url}/v1/billing/internal/subscription/{uid}",
99
+ headers=self._headers(), timeout=10)
100
+ else:
101
+ resp = await client.get(
102
+ f"{self._gateway_url}/v1/billing/subscription",
103
+ headers=self._headers(), timeout=10)
104
+ resp.raise_for_status()
105
+ data = resp.json()
106
+ return SubscriptionInfo(
107
+ plan=data.get("plan", "free"),
108
+ status=data.get("status", "unknown"),
109
+ started_at=data.get("started_at"),
110
+ expires_at=data.get("expires_at"),
111
+ )
112
+ except Exception as e:
113
+ log.warning("Billing get_subscription failed: %s", e)
114
+ return SubscriptionInfo(plan="unknown", status="unavailable")
115
+
116
+ async def track_usage(self, meter: str, quantity: int = 1, user: Any = None) -> bool:
117
+ """Track usage for a given meter. Returns True on success.
118
+
119
+ Requires service_token — usage tracking is a server-to-server operation.
120
+ The gateway endpoint is POST /v1/billing/internal/usage/track (service
121
+ token required). There is no public user-facing track endpoint; calling
122
+ without a service token logs a warning and returns False.
123
+ """
124
+ uid = self._uid(user)
125
+ if not self._service_token:
126
+ log.warning(
127
+ "Billing track_usage requires a service token; "
128
+ "no-token path is not supported (no public track endpoint). "
129
+ "Returning False without attempting the request."
130
+ )
131
+ return False
132
+ try:
133
+ async with httpx.AsyncClient() as client:
134
+ payload: dict = {"meter": meter, "quantity": quantity}
135
+ if uid:
136
+ payload["user_id"] = uid
137
+ payload["tenant_id"] = "default"
138
+ resp = await client.post(
139
+ f"{self._gateway_url}/v1/billing/internal/usage/track",
140
+ json=payload, headers=self._headers(), timeout=10)
141
+ return resp.status_code == 200
142
+ except Exception as e:
143
+ log.warning("Billing track_usage failed: %s", e)
144
+ return False
145
+
146
+ async def get_balance(self, user: Any = None) -> BalanceInfo:
147
+ """Get current token balance and plan cap."""
148
+ uid = self._uid(user)
149
+ try:
150
+ async with httpx.AsyncClient() as client:
151
+ if self._service_token and uid:
152
+ resp = await client.get(
153
+ f"{self._gateway_url}/v1/billing/internal/balance/{uid}",
154
+ headers=self._headers(), timeout=10)
155
+ else:
156
+ resp = await client.get(
157
+ f"{self._gateway_url}/v1/billing/balance",
158
+ headers=self._headers(), timeout=10)
159
+ resp.raise_for_status()
160
+ data = resp.json()
161
+ return BalanceInfo(
162
+ balance=data.get("balance", 0),
163
+ plan=data.get("plan", ""),
164
+ cap=data.get("cap", 0),
165
+ )
166
+ except Exception as e:
167
+ log.warning("Billing get_balance failed: %s", e)
168
+ return BalanceInfo(balance=0, plan="unknown", cap=0)
169
+
170
+ # ─── Payment methods + plan changes + top-up + payment history ──────── #
171
+
172
+ async def list_payment_methods(self, user: Any = None) -> list[PaymentMethod]:
173
+ uid = self._uid(user)
174
+ try:
175
+ async with httpx.AsyncClient() as client:
176
+ url = (f"{self._gateway_url}/v1/billing/internal/payment-methods/{uid}"
177
+ if (self._service_token and uid)
178
+ else f"{self._gateway_url}/v1/billing/payment-methods")
179
+ resp = await client.get(url, headers=self._headers(), timeout=10)
180
+ resp.raise_for_status()
181
+ return [PaymentMethod(**m) for m in resp.json()]
182
+ except Exception as e:
183
+ log.warning("Billing list_payment_methods failed: %s", e)
184
+ return []
185
+
186
+ async def list_payments(self, user: Any = None, limit: int = 50, offset: int = 0) -> list[PaymentRecord]:
187
+ uid = self._uid(user)
188
+ try:
189
+ async with httpx.AsyncClient() as client:
190
+ if self._service_token and uid:
191
+ url = f"{self._gateway_url}/v1/billing/internal/payments/{uid}"
192
+ else:
193
+ url = f"{self._gateway_url}/v1/billing/payments"
194
+ resp = await client.get(url, headers=self._headers(),
195
+ params={"limit": limit, "offset": offset}, timeout=15)
196
+ resp.raise_for_status()
197
+ return [PaymentRecord(**p) for p in resp.json()]
198
+ except Exception as e:
199
+ log.warning("Billing list_payments failed: %s", e)
200
+ return []
201
+
202
+ async def create_setup_intent(self, user: Any = None) -> SetupIntentResult:
203
+ """Add-card SetupIntent. Surfaces errors (the ext needs the client secret)."""
204
+ async with httpx.AsyncClient() as client:
205
+ resp = await client.post(f"{self._gateway_url}/v1/billing/payment-methods/setup",
206
+ headers=self._headers(), timeout=10)
207
+ resp.raise_for_status()
208
+ d = resp.json()
209
+ return SetupIntentResult(client_secret=d.get("client_secret", ""),
210
+ publishable_key=d.get("publishable_key", ""))
211
+
212
+ async def set_default_payment_method(self, pm_id: str, user: Any = None) -> bool:
213
+ async with httpx.AsyncClient() as client:
214
+ resp = await client.put(f"{self._gateway_url}/v1/billing/payment-methods/{pm_id}/default",
215
+ headers=self._headers(), timeout=10)
216
+ resp.raise_for_status()
217
+ return True
218
+
219
+ async def remove_payment_method(self, pm_id: str, user: Any = None) -> bool:
220
+ async with httpx.AsyncClient() as client:
221
+ resp = await client.delete(f"{self._gateway_url}/v1/billing/payment-methods/{pm_id}",
222
+ headers=self._headers(), timeout=10)
223
+ resp.raise_for_status()
224
+ return True
225
+
226
+ async def change_plan(self, plan_id: str, period: str = "monthly", user: Any = None) -> ChangePlanResult:
227
+ """Upgrade (prorated, immediate) / downgrade (scheduled). Surfaces errors."""
228
+ async with httpx.AsyncClient() as client:
229
+ resp = await client.post(f"{self._gateway_url}/v1/billing/change-plan",
230
+ json={"plan_id": plan_id, "period": period},
231
+ headers=self._headers(), timeout=15)
232
+ resp.raise_for_status()
233
+ d = resp.json()
234
+ return ChangePlanResult(
235
+ action=d.get("action", ""), plan=d.get("plan", ""),
236
+ succeeded=bool(d.get("succeeded", False)),
237
+ requires_action=bool(d.get("requires_action", False)),
238
+ client_secret=d.get("client_secret", ""),
239
+ effective_at=d.get("effective_at", "") or "",
240
+ pending=bool(d.get("pending", False)))
241
+
242
+ async def topup(self, tokens: int, price_cents: int, save_payment_method: bool = True,
243
+ off_session: bool = True, user: Any = None) -> TopupResult:
244
+ """Token top-up PaymentIntent. Surfaces errors.
245
+
246
+ With ``off_session=True`` (default) the gateway attempts an immediate
247
+ off-session charge against the saved card and returns ``succeeded`` /
248
+ ``requires_action``. The Element path (``off_session=False``) returns
249
+ the ``client_secret`` instead.
250
+ """
251
+ async with httpx.AsyncClient() as client:
252
+ resp = await client.post(f"{self._gateway_url}/v1/billing/topup",
253
+ json={"tokens": tokens, "price_cents": price_cents,
254
+ "save_payment_method": save_payment_method,
255
+ "off_session": off_session},
256
+ headers=self._headers(), timeout=15)
257
+ resp.raise_for_status()
258
+ d = resp.json()
259
+ return TopupResult(client_secret=d.get("client_secret", ""),
260
+ payment_intent_id=d.get("payment_intent_id", ""),
261
+ publishable_key=d.get("publishable_key", ""),
262
+ succeeded=bool(d.get("succeeded", False)),
263
+ requires_action=bool(d.get("requires_action", False)))
264
+
265
+ async def create_billing_portal_session(self, user: Any = None) -> str:
266
+ """Create a Stripe Customer Portal session and return its hosted URL.
267
+ Surfaces errors (the ext needs the URL)."""
268
+ async with httpx.AsyncClient() as client:
269
+ resp = await client.post(f"{self._gateway_url}/v1/billing/portal",
270
+ headers=self._headers(), timeout=10)
271
+ resp.raise_for_status()
272
+ return resp.json().get("url", "")
273
+
274
+ async def list_plans(self, user: Any = None) -> list[PlanInfo]:
275
+ """Public plan catalog. Safe-degrades to []."""
276
+ try:
277
+ async with httpx.AsyncClient() as client:
278
+ resp = await client.get(f"{self._gateway_url}/v1/billing/plans",
279
+ headers=self._headers(), timeout=10)
280
+ resp.raise_for_status()
281
+ return [PlanInfo(**p) for p in resp.json()]
282
+ except Exception as e:
283
+ log.warning("Billing list_plans failed: %s", e)
284
+ return []
285
+
286
+ async def get_auto_topup(self, user: Any = None) -> AutoTopupSettings:
287
+ """Safe-degrades to disabled defaults."""
288
+ uid = self._uid(user)
289
+ try:
290
+ async with httpx.AsyncClient() as client:
291
+ resp = await client.get(f"{self._gateway_url}/v1/billing/auto-topup",
292
+ headers=self._headers(), timeout=10)
293
+ resp.raise_for_status()
294
+ return AutoTopupSettings(**resp.json())
295
+ except Exception as e:
296
+ log.warning("Billing get_auto_topup failed: %s", e)
297
+ return AutoTopupSettings()
298
+
299
+ async def set_auto_topup(self, enabled: bool, threshold_pct: int = 10,
300
+ recharge_tokens: int = 20000, payment_method_id: str = "",
301
+ user: Any = None) -> bool:
302
+ """Surfaces errors."""
303
+ async with httpx.AsyncClient() as client:
304
+ resp = await client.put(f"{self._gateway_url}/v1/billing/auto-topup",
305
+ json={"enabled": enabled, "threshold_pct": threshold_pct,
306
+ "recharge_tokens": recharge_tokens, "payment_method_id": payment_method_id},
307
+ headers=self._headers(), timeout=10)
308
+ resp.raise_for_status()
309
+ return True
310
+
311
+ async def cancel_subscription(self, user: Any = None) -> dict:
312
+ """Cancel at period end. Surfaces errors. Returns the gateway result dict."""
313
+ async with httpx.AsyncClient() as client:
314
+ resp = await client.post(f"{self._gateway_url}/v1/billing/cancel",
315
+ headers=self._headers(), timeout=15)
316
+ resp.raise_for_status()
317
+ return resp.json() if resp.content else {}
318
+
319
+ async def update_billing_profile(self, profile: dict, user: Any = None) -> bool:
320
+ """Surfaces errors. profile keys: name/company/vat/country."""
321
+ async with httpx.AsyncClient() as client:
322
+ resp = await client.put(f"{self._gateway_url}/v1/billing/profile",
323
+ json=profile, headers=self._headers(), timeout=10)
324
+ resp.raise_for_status()
325
+ return True
@@ -50,6 +50,19 @@ class BillingProtocol(Protocol):
50
50
  async def get_subscription(self) -> SubscriptionInfo: ...
51
51
  async def track_usage(self, meter: str, quantity: int = 1, user: Any = None) -> bool: ...
52
52
  async def get_balance(self) -> BalanceInfo: ...
53
+ async def list_payment_methods(self, user: Any = None) -> list: ...
54
+ async def list_payments(self, user: Any = None, limit: int = 50, offset: int = 0) -> list: ...
55
+ async def create_setup_intent(self, user: Any = None): ...
56
+ async def set_default_payment_method(self, pm_id: str, user: Any = None) -> bool: ...
57
+ async def remove_payment_method(self, pm_id: str, user: Any = None) -> bool: ...
58
+ async def change_plan(self, plan_id: str, period: str = "monthly", user: Any = None): ...
59
+ async def topup(self, tokens: int, price_cents: int, save_payment_method: bool = True, off_session: bool = True, user: Any = None): ...
60
+ async def create_billing_portal_session(self, user: Any = None) -> str: ...
61
+ async def list_plans(self, user: Any = None) -> list: ...
62
+ async def get_auto_topup(self, user: Any = None): ...
63
+ async def set_auto_topup(self, enabled: bool, threshold_pct: int = 10, recharge_tokens: int = 20000, payment_method_id: str = "", user: Any = None) -> bool: ...
64
+ async def cancel_subscription(self, user: Any = None) -> dict: ...
65
+ async def update_billing_profile(self, profile: dict, user: Any = None) -> bool: ...
53
66
 
54
67
 
55
68
  @runtime_checkable
@@ -35,6 +35,9 @@ _DECORATOR_ROLES = {
35
35
  # stale 'amount' field must never reappear here.
36
36
  _HTTP_PAYLOADS: dict[str, list[str]] = {
37
37
  "POST /v1/billing/internal/usage/track": ["meter", "quantity", "user_id", "tenant_id"],
38
+ "POST /v1/billing/change-plan": ["plan_id", "period"],
39
+ "POST /v1/billing/topup": ["tokens", "price_cents", "save_payment_method", "off_session"],
40
+ "POST /v1/billing/payment-methods/setup": [],
38
41
  }
39
42
 
40
43
 
@@ -123,3 +123,79 @@ class HTTPStatusError(Exception):
123
123
  self.status_code = status_code
124
124
  self.body = body or ""
125
125
  super().__init__(f"HTTP {status_code}: {str(body)[:200]}")
126
+
127
+
128
+ @dataclass
129
+ class PaymentMethod:
130
+ """Result item from ctx.billing.list_payment_methods()."""
131
+ id: str = ""
132
+ type: str = "card"
133
+ brand: str = ""
134
+ last4: str = ""
135
+ exp_month: int = 0
136
+ exp_year: int = 0
137
+ is_default: bool = False
138
+
139
+
140
+ @dataclass
141
+ class SetupIntentResult:
142
+ """Result from ctx.billing.create_setup_intent() — drives a Stripe add-card flow."""
143
+ client_secret: str = ""
144
+ publishable_key: str = ""
145
+
146
+
147
+ @dataclass
148
+ class ChangePlanResult:
149
+ """Result from ctx.billing.change_plan()."""
150
+ action: str = ""
151
+ plan: str = ""
152
+ succeeded: bool = False
153
+ requires_action: bool = False
154
+ client_secret: str = ""
155
+ effective_at: str = ""
156
+ pending: bool = False
157
+
158
+
159
+ @dataclass
160
+ class TopupResult:
161
+ """Result from ctx.billing.topup() — drives a Stripe Payment Element."""
162
+ client_secret: str = ""
163
+ payment_intent_id: str = ""
164
+ publishable_key: str = ""
165
+ succeeded: bool = False
166
+ requires_action: bool = False
167
+
168
+
169
+ @dataclass
170
+ class PaymentRecord:
171
+ """Result item from ctx.billing.list_payments()."""
172
+ payment_intent_id: str = ""
173
+ amount_cents: int = 0
174
+ currency: str = "usd"
175
+ tokens: int = 0
176
+ status: str = ""
177
+ type: str = ""
178
+ created_at: str | None = None
179
+ completed_at: str | None = None
180
+ receipt_url: str = ""
181
+
182
+
183
+ @dataclass
184
+ class PlanInfo:
185
+ """Result item from ctx.billing.list_plans() — the public plan catalog."""
186
+ id: str = ""
187
+ name: str = ""
188
+ price: float = 0.0
189
+ interval: str = "monthly"
190
+ features: dict = field(default_factory=dict)
191
+ limits: dict = field(default_factory=dict)
192
+
193
+
194
+ @dataclass
195
+ class AutoTopupSettings:
196
+ """Result from ctx.billing.get_auto_topup() — auto-recharge configuration."""
197
+ enabled: bool = False
198
+ threshold_pct: int = 10
199
+ recharge_tokens: int = 20000
200
+ recharge_cents: int = 2000
201
+ payment_method_id: str = ""
@@ -0,0 +1,101 @@
1
+ """Conformance A8/A9 — committed contract artifacts must match the live SDK.
2
+
3
+ A8 (audit 2026-06-04, re-validated 2026-06-12): ``sdk_claims.json`` sat at
4
+ 5.1.0 while the SDK shipped 5.2.0/5.2.1/5.2.2 — the kernel-side
5
+ ``contract_guard`` freshness layer keyed on the stale pin and the lag was
6
+ invisible to every local gate. A9: the docs_guard snapshots
7
+ (``api_surface.json`` / ``ctx_surface.json``) had the same hand-copied-rot
8
+ failure mode. These tests run under preflight Edge 1 (``tests/contract/``),
9
+ so a stale committed artifact turns the one sanctioned gate red locally.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import dataclasses
14
+ import json
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ from imperal_sdk.devtools.generate_api_surface import generate_surface
20
+ from imperal_sdk.devtools.generate_sdk_claims import generate_claims
21
+
22
+ REPO = Path(__file__).resolve().parents[2]
23
+ # imperal-sdk sits inside the MCP-Configs workspace checkout; standalone
24
+ # clones (CI on GitHub) won't have the workspace-level docs_guard inputs.
25
+ WORKSPACE_GUARD_INPUTS = REPO.parent / "scripts" / "docs_guard" / "inputs"
26
+
27
+
28
+ def test_committed_sdk_claims_match_generated() -> None:
29
+ committed = json.loads((REPO / "sdk_claims.json").read_text(encoding="utf-8"))
30
+ assert committed == generate_claims(), (
31
+ "sdk_claims.json is stale — regenerate with: "
32
+ "python -m imperal_sdk.devtools.generate_sdk_claims --output sdk_claims.json "
33
+ "(and copy to kernel tools/contract/sdk-claims.json on deploy)"
34
+ )
35
+
36
+
37
+ def test_docs_guard_api_surface_snapshot_fresh() -> None:
38
+ snap = WORKSPACE_GUARD_INPUTS / "api_surface.json"
39
+ if not snap.is_file():
40
+ pytest.skip("workspace docs_guard inputs not present (standalone SDK checkout)")
41
+ committed = json.loads(snap.read_text(encoding="utf-8"))
42
+ live = generate_surface()
43
+ assert {ns: sorted(m) for ns, m in committed.items()} == {
44
+ ns: sorted(m) for ns, m in live.items()
45
+ }, (
46
+ "scripts/docs_guard/inputs/api_surface.json is stale — refresh it from "
47
+ "imperal_sdk.devtools.generate_api_surface (manual-cp rot, conformance A9)"
48
+ )
49
+
50
+
51
+ def _live_ctx_surface() -> set[str]:
52
+ """Public Context surface: dataclass fields + properties + methods.
53
+
54
+ Mirrors the original ctx_surface.json snapshot semantics (everything an
55
+ extension author can legitimately write after ``ctx.`` at the top level,
56
+ excluding kernel-injected attrs like ``secrets`` which are not part of
57
+ the dataclass).
58
+ """
59
+ from imperal_sdk.context import Context
60
+
61
+ fields = {f.name for f in dataclasses.fields(Context) if not f.name.startswith("_")}
62
+ members = {
63
+ name
64
+ for name, value in vars(Context).items()
65
+ if not name.startswith("_") and (isinstance(value, property) or callable(value))
66
+ }
67
+ return fields | members
68
+
69
+
70
+ def test_kernel_contract_copies_mutually_consistent() -> None:
71
+ """A5: the two local copies of the kernel contract (the SDK contract-test
72
+ fixture and the docs_guard snapshot) must be identical — both are refreshed
73
+ from kernel ``tools/kernel-contract.json`` on a kernel contract change, and
74
+ one-sided rot (e.g. the pre-2026-06-12 state: guard copy carried the old
75
+ max_depth=3 while the fixture carried 6) is exactly how 'ALL GREEN'
76
+ overstates coverage."""
77
+ guard_copy = WORKSPACE_GUARD_INPUTS / "kernel-contract.json"
78
+ if not guard_copy.is_file():
79
+ pytest.skip("workspace docs_guard inputs not present (standalone SDK checkout)")
80
+ fixture = json.loads(
81
+ (REPO / "tests" / "fixtures" / "contract" / "kernel-contract.sample.json").read_text(
82
+ encoding="utf-8"
83
+ )
84
+ )
85
+ assert fixture == json.loads(guard_copy.read_text(encoding="utf-8")), (
86
+ "kernel-contract fixture and docs_guard snapshot diverged — refresh BOTH "
87
+ "from kernel tools/kernel-contract.json (conformance A5)"
88
+ )
89
+
90
+
91
+ def test_docs_guard_ctx_surface_snapshot_fresh() -> None:
92
+ snap = WORKSPACE_GUARD_INPUTS / "ctx_surface.json"
93
+ if not snap.is_file():
94
+ pytest.skip("workspace docs_guard inputs not present (standalone SDK checkout)")
95
+ committed = set(json.loads(snap.read_text(encoding="utf-8")))
96
+ live = _live_ctx_surface()
97
+ assert committed == live, (
98
+ f"scripts/docs_guard/inputs/ctx_surface.json is stale (conformance A9). "
99
+ f"missing_from_snapshot={sorted(live - committed)} "
100
+ f"gone_from_sdk={sorted(committed - live)}"
101
+ )