imperal-sdk 4.2.10__tar.gz → 4.2.12__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 (210) hide show
  1. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/CHANGELOG.md +71 -0
  2. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/PKG-INFO +1 -1
  3. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/__init__.py +1 -1
  4. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/context.py +102 -0
  5. imperal_sdk-4.2.12/src/imperal_sdk/http/client.py +86 -0
  6. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/interactive.py +17 -3
  7. imperal_sdk-4.2.12/tests/test_context_background_task.py +76 -0
  8. imperal_sdk-4.2.12/tests/test_context_deliver_chat_message.py +96 -0
  9. imperal_sdk-4.2.12/tests/test_http_timeout_override.py +59 -0
  10. imperal_sdk-4.2.10/src/imperal_sdk/http/client.py +0 -51
  11. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.github/workflows/identity-contract.yml +0 -0
  12. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.github/workflows/publish.yml +0 -0
  13. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.github/workflows/test.yml +0 -0
  14. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.gitignore +0 -0
  15. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/LICENSE +0 -0
  16. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/README.md +0 -0
  17. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/api_surface.json +0 -0
  18. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/pyproject.toml +0 -0
  19. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/.codebase-index-cache.pkl +0 -0
  20. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ai/__init__.py +0 -0
  21. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ai/client.py +0 -0
  22. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/auth/__init__.py +0 -0
  23. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/auth/client.py +0 -0
  24. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/auth/middleware.py +0 -0
  25. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/billing/__init__.py +0 -0
  26. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/billing/client.py +0 -0
  27. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cache/__init__.py +0 -0
  28. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cache/client.py +0 -0
  29. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cache/protocol.py +0 -0
  30. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/__init__.py +0 -0
  31. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/action_result.py +0 -0
  32. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/error_codes.py +0 -0
  33. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/extension.py +0 -0
  34. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/filters.py +0 -0
  35. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/guards.py +0 -0
  36. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/handler.py +0 -0
  37. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/kernel_primitives.py +0 -0
  38. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/narration.py +0 -0
  39. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/narration_guard.py +0 -0
  40. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/prompt.py +0 -0
  41. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/refusal.py +0 -0
  42. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/retry.py +0 -0
  43. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cli/__init__.py +0 -0
  44. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cli/main.py +0 -0
  45. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/config/__init__.py +0 -0
  46. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/config/client.py +0 -0
  47. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/db/__init__.py +0 -0
  48. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/db/client.py +0 -0
  49. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/errors.py +0 -0
  50. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/extension.py +0 -0
  51. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/extensions/__init__.py +0 -0
  52. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/extensions/client.py +0 -0
  53. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/http/__init__.py +0 -0
  54. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/manifest.py +0 -0
  55. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/manifest_schema.py +0 -0
  56. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/notify/__init__.py +0 -0
  57. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/notify/client.py +0 -0
  58. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/__init__.py +0 -0
  59. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/icnli_integrity_rules.txt +0 -0
  60. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/kernel_formatting_rule.txt +0 -0
  61. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/kernel_proactivity_rule.txt +0 -0
  62. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/protocols.py +0 -0
  63. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/rpc/__init__.py +0 -0
  64. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/rpc/codec.py +0 -0
  65. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/rpc/contract.py +0 -0
  66. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/__init__.py +0 -0
  67. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/executor.py +0 -0
  68. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/llm_provider.py +0 -0
  69. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/message_adapter.py +0 -0
  70. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/action_result.schema.json +0 -0
  71. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/balance_info.schema.json +0 -0
  72. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/chat_result.schema.json +0 -0
  73. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/completion_result.schema.json +0 -0
  74. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/document.schema.json +0 -0
  75. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/event.schema.json +0 -0
  76. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/file_info.schema.json +0 -0
  77. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/function_call.schema.json +0 -0
  78. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/http_response.schema.json +0 -0
  79. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/imperal.schema.json +0 -0
  80. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/limits_result.schema.json +0 -0
  81. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/subscription_info.schema.json +0 -0
  82. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/__init__.py +0 -0
  83. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/client.py +0 -0
  84. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/exceptions.py +0 -0
  85. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/panel_handler.py +0 -0
  86. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/spec.py +0 -0
  87. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/security/__init__.py +0 -0
  88. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/security/call_token.py +0 -0
  89. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/skeleton/__init__.py +0 -0
  90. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/skeleton/client.py +0 -0
  91. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/storage/__init__.py +0 -0
  92. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/storage/client.py +0 -0
  93. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/store/__init__.py +0 -0
  94. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/store/client.py +0 -0
  95. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/store/exceptions.py +0 -0
  96. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/testing/__init__.py +0 -0
  97. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/testing/mock_context.py +0 -0
  98. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/testing/mock_secrets.py +0 -0
  99. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/__init__.py +0 -0
  100. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/client.py +0 -0
  101. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/generate_api_surface.py +0 -0
  102. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/validate_identity_contract.py +0 -0
  103. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/__init__.py +0 -0
  104. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/action_result.py +0 -0
  105. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/chat_result.py +0 -0
  106. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/client_contracts.py +0 -0
  107. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/contracts.py +0 -0
  108. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/contributions.py +0 -0
  109. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/events.py +0 -0
  110. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/health.py +0 -0
  111. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/identity.py +0 -0
  112. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/models.py +0 -0
  113. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/pagination.py +0 -0
  114. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/store_contracts.py +0 -0
  115. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/__init__.py +0 -0
  116. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/actions.py +0 -0
  117. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/base.py +0 -0
  118. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/data.py +0 -0
  119. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/display.py +0 -0
  120. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/feedback.py +0 -0
  121. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/graph.py +0 -0
  122. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/input_components.py +0 -0
  123. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/layout.py +0 -0
  124. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/theme.py +0 -0
  125. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/validator.py +0 -0
  126. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/validator_v1_6_0.py +0 -0
  127. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/conftest.py +0 -0
  128. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/contracts/__init__.py +0 -0
  129. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/contracts/test_store_contracts.py +0 -0
  130. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/fixtures/openapi/auth-gateway.json +0 -0
  131. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/fixtures/openapi/registry.json +0 -0
  132. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/fixtures/openapi/sharelock-cases.json +0 -0
  133. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/rpc/__init__.py +0 -0
  134. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/rpc/test_codec.py +0 -0
  135. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/rpc/test_contract.py +0 -0
  136. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/runtime/__init__.py +0 -0
  137. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/runtime/test_llm_provider_config_store.py +0 -0
  138. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/runtime/test_llm_provider_ctx_injection.py +0 -0
  139. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/store/__init__.py +0 -0
  140. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/store/test_list_users_client.py +0 -0
  141. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/store/test_query_all_client.py +0 -0
  142. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_action_result_typed.py +0 -0
  143. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_as_user.py +0 -0
  144. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_auth.py +0 -0
  145. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_billing.py +0 -0
  146. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_cache_client.py +0 -0
  147. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_cache_model.py +0 -0
  148. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_call_token.py +0 -0
  149. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_extension_deprecation.py +0 -0
  150. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_filters.py +0 -0
  151. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_guards.py +0 -0
  152. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_guards_bleed.py +0 -0
  153. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_prompt.py +0 -0
  154. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_pydantic_retry.py +0 -0
  155. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_result.py +0 -0
  156. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_cli.py +0 -0
  157. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_client_contracts.py +0 -0
  158. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_config_client.py +0 -0
  159. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_context.py +0 -0
  160. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_context_guards.py +0 -0
  161. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_contracts.py +0 -0
  162. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_contracts_live.py +0 -0
  163. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_contributions.py +0 -0
  164. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_document_contract.py +0 -0
  165. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_emits_decorator.py +0 -0
  166. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_error_codes.py +0 -0
  167. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_errors.py +0 -0
  168. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_event_schema_v2.py +0 -0
  169. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_events_health.py +0 -0
  170. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_extension.py +0 -0
  171. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_extension_v2.py +0 -0
  172. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_extensions_emit.py +0 -0
  173. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_handler_p2.py +0 -0
  174. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_id_shape_guard.py +0 -0
  175. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_identity_contract.py +0 -0
  176. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_imperal_schema_v2.py +0 -0
  177. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_kernel_primitives.py +0 -0
  178. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest.py +0 -0
  179. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_roundtrip_gate.py +0 -0
  180. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_schema.py +0 -0
  181. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_v2_events.py +0 -0
  182. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_v2_other_sections.py +0 -0
  183. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_v2_webhooks.py +0 -0
  184. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_validator_v2.py +0 -0
  185. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_mock_context.py +0 -0
  186. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_models.py +0 -0
  187. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_narration_emission.py +0 -0
  188. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_narration_guard.py +0 -0
  189. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_openai_max_completion_tokens.py +0 -0
  190. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_pagination.py +0 -0
  191. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_panel_rendering_contract.py +0 -0
  192. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_panels.py +0 -0
  193. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_skeleton_decorator.py +0 -0
  194. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_spec_validation.py +0 -0
  195. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_tools_client.py +0 -0
  196. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui.py +0 -0
  197. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_fileupload_enhanced.py +0 -0
  198. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_html.py +0 -0
  199. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_image_enhanced.py +0 -0
  200. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_open.py +0 -0
  201. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_theme.py +0 -0
  202. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_user.py +0 -0
  203. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_v7_emit_refusal.py +0 -0
  204. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator.py +0 -0
  205. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator_drift.py +0 -0
  206. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator_pep563.py +0 -0
  207. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator_v1_6_0_rules.py +0 -0
  208. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_write_arg_bleed.py +0 -0
  209. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/tools/__init__.py +0 -0
  210. {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/tools/test_generate_api_surface.py +0 -0
@@ -2,6 +2,77 @@
2
2
 
3
3
  All notable changes to `imperal-sdk` are documented here.
4
4
 
5
+ ## 4.2.12 — 2026-05-14
6
+
7
+ **Feat: LONGRUN-V1 Session 1 — long-running operations primitives**
8
+
9
+ Three new SDK surfaces for ops that exceed the 30s `ctx.http` ceiling:
10
+
11
+ ### Added
12
+
13
+ - **`ctx.http.{get,post,put,patch,delete}(..., timeout=N)` per-call kwarg.**
14
+ Federal cap 180s (`I-LONGRUN-HTTP-CAP-180S`). Anything larger raises
15
+ `ValueError("ctx.http timeout {N}s exceeds federal cap (180s)...")` —
16
+ use `ctx.background_task()` for longer ops.
17
+
18
+ - **`ctx.background_task(coro, *, long_running=False, name="") -> str`**
19
+ — explicit opt-in for the kernel's auto-promote path. Coro runs detached
20
+ via `asyncio.create_task`; kernel auto-delivers its returned
21
+ `ActionResult` as a fresh bot message to the user's chat when done.
22
+ `long_running=True` raises the 180s cap to 1800s
23
+ (`LONG_RUNNING_TASK_S`). Returns task_id immediately.
24
+
25
+ - **`ctx.deliver_chat_message(text, *, msg_type="response", refresh_panels=None)`**
26
+ — public API for extension-initiated bot turn injection. Posts to
27
+ auth-gw `/v1/internal/chat/inject`. Text truncated to 64KB with marker.
28
+ `msg_type` ∈ `{response, system, tool_result}`.
29
+
30
+ ### Federal invariants (new)
31
+
32
+ - `I-LONGRUN-HTTP-CAP-180S` — per-call timeout federal cap
33
+ - `I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT` — coro contract enforced
34
+ kernel-side via `_explicit_background_task_completion` isinstance
35
+ check; violation writes critical audit row + delivers fallback error.
36
+ - `I-LONGRUN-BG-USER-SCOPED` — task bound to `(ext_id, user_id)` at
37
+ creation (existing `util/task_manager.py` invariant).
38
+ - `I-LONGRUN-CHAT-INJECT-USER-SCOPED` — `X-Acting-User` header must
39
+ match `body.user_id` (auth-gw 403 on mismatch).
40
+ - `I-LONGRUN-CHAT-INJECT-AUDIT-EVERY` — every inject writes an
41
+ `action_ledger` row through the `write_audit` chokepoint.
42
+
43
+ ### Spec
44
+
45
+ `superpowers/specs/2026-05-13-longrun-v1-design.md`. Sessions 2 (Sharelock
46
+ canary migration) and 3 (docs + observability) ship after 24h soak.
47
+
48
+ ### Migration
49
+
50
+ None required — all additions are additive opt-in. Existing extensions
51
+ work unchanged.
52
+
53
+ ## 4.2.11 — 2026-05-13
54
+
55
+ **Fix: `ui.Link(text=...)` no longer breaks panel render**
56
+
57
+ `ui.Link` now accepts the visible text via either `label=` (canonical) or
58
+ `text=` (alias matching the HTML/JSX `<a>text</a>` mental model). Before
59
+ v4.2.11, passing `text=` raised `TypeError: Link() got an unexpected
60
+ keyword argument 'text'` at panel-render time, which killed the right
61
+ panel for any extension that used the natural-looking kwarg.
62
+
63
+ ```python
64
+ # All three are now equivalent
65
+ ui.Link("Read docs", href="https://docs.imperal.io") # positional
66
+ ui.Link(label="Read docs", href="https://docs.imperal.io") # canonical kwarg
67
+ ui.Link(text="Read docs", href="https://docs.imperal.io") # alias kwarg
68
+ ```
69
+
70
+ Calling `ui.Link()` with neither `label` nor `text` raises a clear
71
+ `TypeError("ui.Link requires a 'label' (or 'text' alias)")` instead of
72
+ silently rendering an empty anchor. `label` wins if both are passed.
73
+
74
+ No migration required — existing `label=` callers are unchanged.
75
+
5
76
  ## 4.2.10 — 2026-05-13
6
77
 
7
78
  **Feat: `@chat.function` default `chain_callable=True` for ALL action_types (was write/destructive only)**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imperal-sdk
3
- Version: 4.2.10
3
+ Version: 4.2.12
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
@@ -39,7 +39,7 @@ from imperal_sdk.secrets import (
39
39
  SecretValueTooLarge, SecretDeclarationConflict,
40
40
  )
41
41
 
42
- __version__ = "4.2.10"
42
+ __version__ = "4.2.12"
43
43
 
44
44
  __all__ = [
45
45
  # Core
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
6
6
 
7
+ import httpx
8
+
7
9
  from imperal_sdk.types.identity import UserContext, TenantContext
8
10
 
9
11
  if TYPE_CHECKING:
@@ -290,6 +292,106 @@ class Context:
290
292
  if cb:
291
293
  await cb(percent, message)
292
294
 
295
+ async def background_task(
296
+ self,
297
+ coro,
298
+ *,
299
+ long_running: bool = False,
300
+ name: str = "",
301
+ ) -> str:
302
+ """Spawn a coroutine in the background; return task_id immediately.
303
+
304
+ The kernel detaches ``coro`` via ``asyncio.create_task`` and tracks
305
+ it as an explicit-opt-in background task (federal task lifecycle in
306
+ ``util/task_manager.py``). When ``coro`` completes, the kernel
307
+ auto-delivers its returned ``ActionResult`` as a fresh bot message
308
+ in the user's chat via the existing ``_deliver_to_chat`` pipeline.
309
+
310
+ Args:
311
+ coro: A coroutine returning ``ActionResult``. Required.
312
+ long_running: ``False`` (default) → 180s federal cap.
313
+ ``True`` → 1800s cap (``LONG_RUNNING_TASK_S``).
314
+ name: Human-readable name for UI/audit (default ``"background"``).
315
+
316
+ Returns:
317
+ task_id (str) — federal Redis key ``imperal:task:{task_id}``.
318
+
319
+ Raises:
320
+ RuntimeError: if ``ctx`` lacks kernel-injected spawn hook
321
+ (e.g. dev mode or non-extension dispatch context).
322
+
323
+ Federal: ``I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT`` — ``coro`` MUST
324
+ return ``ActionResult``; non-ActionResult return triggers a critical
325
+ audit row and delivers a fallback error to chat.
326
+ """
327
+ spawn = getattr(self, "_background_task_spawn", None)
328
+ if spawn is None:
329
+ raise RuntimeError(
330
+ "ctx.background_task not available in this context — the "
331
+ "Context was constructed without a kernel-injected spawn "
332
+ "hook. This is typically a test harness or dev-mode issue."
333
+ )
334
+ return await spawn(coro, long_running=long_running, name=name or "background")
335
+
336
+ async def deliver_chat_message(
337
+ self,
338
+ text: str,
339
+ *,
340
+ msg_type: str = "response",
341
+ refresh_panels: list[str] | None = None,
342
+ ) -> None:
343
+ """Inject a bot message into user's chat at any time.
344
+
345
+ Mirrors the existing kernel auto-promote chat-injection path
346
+ (``pipeline/task_delivery.py:_deliver_to_chat``) but is callable
347
+ explicitly from extension code. Use cases:
348
+ - Background task multi-stage announcements before final result
349
+ - Webhook acknowledgements ("Spotify connected!")
350
+ - Any one-off bot message that skeleton-alert polling can't model
351
+
352
+ Args:
353
+ text: Bot message body (Markdown). Truncated to 64KB if larger.
354
+ msg_type: ``"response"`` (default — bot turn),
355
+ ``"system"`` (italicized notice),
356
+ ``"tool_result"`` (formatted as tool execution).
357
+ refresh_panels: optional list of panel_ids to re-render after.
358
+
359
+ Federal: ``I-LONGRUN-CHAT-INJECT-USER-SCOPED`` — inject scoped to
360
+ ``(ext_id, user_id)``; cross-user inject returns 403.
361
+ ``I-LONGRUN-CHAT-INJECT-AUDIT-EVERY`` — every inject writes an
362
+ audit row in ``action_ledger``.
363
+ """
364
+ MAX_BYTES = 64 * 1024
365
+ if len(text.encode("utf-8")) > MAX_BYTES:
366
+ text = text.encode("utf-8")[:MAX_BYTES].decode("utf-8", errors="ignore") + "...(truncated)"
367
+
368
+ ext_id = getattr(self, "_extension_id", None)
369
+ user_id = getattr(self, "_user_id", None) or getattr(self.user, "imperal_id", None)
370
+ gateway_url = getattr(self, "_gateway_url", None)
371
+ service_token = getattr(self, "_service_token", None)
372
+ if not all([ext_id, user_id, gateway_url, service_token]):
373
+ raise RuntimeError(
374
+ "ctx.deliver_chat_message requires kernel-injected (ext_id, "
375
+ "user_id, gateway_url, service_token). Not available in this "
376
+ "context (likely a test harness or dev-mode dispatch)."
377
+ )
378
+
379
+ url = f"{gateway_url.rstrip('/')}/v1/internal/chat/inject"
380
+ body = {
381
+ "ext_id": ext_id,
382
+ "user_id": user_id,
383
+ "text": text,
384
+ "msg_type": msg_type,
385
+ }
386
+ if refresh_panels:
387
+ body["refresh_panels"] = refresh_panels
388
+ headers = {
389
+ "X-Service-Token": service_token,
390
+ "X-Acting-User": user_id,
391
+ }
392
+ async with httpx.AsyncClient(timeout=10.0) as c:
393
+ await c.post(url, json=body, headers=headers)
394
+
293
395
  async def log(self, message: str, level: str = "info") -> None:
294
396
  """Structured log visible in extension dashboard."""
295
397
  import logging
@@ -0,0 +1,86 @@
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 httpx
5
+
6
+ from imperal_sdk.types.models import HTTPResponse
7
+
8
+
9
+ def _wrap(resp: httpx.Response) -> HTTPResponse:
10
+ """Convert httpx.Response to typed HTTPResponse."""
11
+ content_type = resp.headers.get("content-type", "")
12
+ if "json" in content_type:
13
+ try:
14
+ body = resp.json()
15
+ except Exception:
16
+ body = resp.text
17
+ elif resp.content:
18
+ try:
19
+ body = resp.text
20
+ except Exception:
21
+ body = resp.content
22
+ else:
23
+ body = ""
24
+ return HTTPResponse(status_code=resp.status_code, body=body, headers=dict(resp.headers))
25
+
26
+
27
+ class HTTPClient:
28
+ """Federal HTTP client.
29
+
30
+ Per-call ``timeout=`` kwarg overrides the instance default. Federal
31
+ cap is 180 seconds (``I-LONGRUN-HTTP-CAP-180S``) — anything longer
32
+ must use :func:`ctx.background_task` instead.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ timeout: float = 30,
38
+ max_redirects: int = 5,
39
+ max_timeout: float = 180,
40
+ ):
41
+ self._timeout = timeout
42
+ self._max_timeout = max_timeout
43
+ self._max_redirects = max_redirects
44
+
45
+ def _resolve_timeout(self, override: float | None) -> float:
46
+ if override is None:
47
+ return self._timeout
48
+ if override <= 0:
49
+ raise ValueError(f"ctx.http timeout must be > 0; got {override}")
50
+ if override > self._max_timeout:
51
+ raise ValueError(
52
+ f"ctx.http timeout {override}s exceeds federal cap "
53
+ f"({self._max_timeout}s). For >180s use ctx.background_task() "
54
+ f"— see concepts/long-running-operations."
55
+ )
56
+ return override
57
+
58
+ async def get(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
59
+ t = self._resolve_timeout(timeout)
60
+ async with httpx.AsyncClient(timeout=t, follow_redirects=True,
61
+ max_redirects=self._max_redirects) as c:
62
+ return _wrap(await c.get(url, **kwargs))
63
+
64
+ async def post(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
65
+ t = self._resolve_timeout(timeout)
66
+ async with httpx.AsyncClient(timeout=t, follow_redirects=True,
67
+ max_redirects=self._max_redirects) as c:
68
+ return _wrap(await c.post(url, **kwargs))
69
+
70
+ async def put(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
71
+ t = self._resolve_timeout(timeout)
72
+ async with httpx.AsyncClient(timeout=t, follow_redirects=True,
73
+ max_redirects=self._max_redirects) as c:
74
+ return _wrap(await c.put(url, **kwargs))
75
+
76
+ async def patch(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
77
+ t = self._resolve_timeout(timeout)
78
+ async with httpx.AsyncClient(timeout=t, follow_redirects=True,
79
+ max_redirects=self._max_redirects) as c:
80
+ return _wrap(await c.patch(url, **kwargs))
81
+
82
+ async def delete(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
83
+ t = self._resolve_timeout(timeout)
84
+ async with httpx.AsyncClient(timeout=t, follow_redirects=True,
85
+ max_redirects=self._max_redirects) as c:
86
+ return _wrap(await c.delete(url, **kwargs))
@@ -73,9 +73,23 @@ def Tooltip(content: str, children: UINode | None = None) -> UINode:
73
73
  return UINode(type="Tooltip", props=props)
74
74
 
75
75
 
76
- def Link(label: str, href: str = "", on_click: UIAction | None = None) -> UINode:
77
- """Hyperlink navigates via href or fires on_click action."""
78
- props: dict[str, Any] = {"label": label}
76
+ def Link(
77
+ label: str = "",
78
+ href: str = "",
79
+ on_click: UIAction | None = None,
80
+ *,
81
+ text: str = "",
82
+ ) -> UINode:
83
+ """Hyperlink — navigates via href or fires on_click action.
84
+
85
+ The visible text can be passed as either ``label`` (canonical) or
86
+ ``text`` (alias matching the HTML/JSX `<a>text</a>` mental model).
87
+ Exactly one must be provided; ``label`` wins if both are set.
88
+ """
89
+ resolved = label or text
90
+ if not resolved:
91
+ raise TypeError("ui.Link requires a 'label' (or 'text' alias)")
92
+ props: dict[str, Any] = {"label": resolved}
79
93
  if href: props["href"] = href
80
94
  if on_click: props["on_click"] = on_click
81
95
  return UINode(type="Link", props=props)
@@ -0,0 +1,76 @@
1
+ # tests/test_context_background_task.py
2
+ """Component B — ctx.background_task() public API surface tests.
3
+
4
+ Verifies the SDK contract:
5
+ - Returns a task_id (str) immediately.
6
+ - Spawns coroutine via injected kernel hook.
7
+ - Raises RuntimeError if no kernel hook present (e.g. dev mode).
8
+ """
9
+ from __future__ import annotations
10
+ import pytest
11
+
12
+ from imperal_sdk import ActionResult
13
+ from imperal_sdk.context import Context
14
+ from imperal_sdk.types.identity import UserContext
15
+
16
+
17
+ def _make_ctx() -> Context:
18
+ return Context(user=UserContext(
19
+ imperal_id="imp_u_test", email="test@test.com", tenant_id="t", role="user",
20
+ ))
21
+
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_background_task_returns_task_id_str():
25
+ captured: dict = {}
26
+
27
+ async def _spawn_hook(coro, *, long_running, name):
28
+ captured["coro_present"] = coro is not None
29
+ captured["long_running"] = long_running
30
+ captured["name"] = name
31
+ coro.close()
32
+ return "task_xyz123"
33
+
34
+ ctx = _make_ctx()
35
+ ctx._background_task_spawn = _spawn_hook # type: ignore[attr-defined]
36
+
37
+ async def _work():
38
+ return ActionResult.success(data={"ok": True})
39
+
40
+ tid = await ctx.background_task(_work(), long_running=True, name="my-task")
41
+ assert tid == "task_xyz123"
42
+ assert captured["long_running"] is True
43
+ assert captured["name"] == "my-task"
44
+ assert captured["coro_present"] is True
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_background_task_raises_without_kernel_hook():
49
+ ctx = _make_ctx()
50
+
51
+ async def _work():
52
+ return ActionResult.success(data={"ok": True})
53
+
54
+ coro = _work()
55
+ with pytest.raises(RuntimeError, match="ctx.background_task not available"):
56
+ await ctx.background_task(coro)
57
+ coro.close()
58
+
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_background_task_default_long_running_false():
62
+ captured: dict = {}
63
+
64
+ async def _spawn_hook(coro, *, long_running, name):
65
+ captured["long_running"] = long_running
66
+ coro.close()
67
+ return "task_y"
68
+
69
+ ctx = _make_ctx()
70
+ ctx._background_task_spawn = _spawn_hook # type: ignore[attr-defined]
71
+
72
+ async def _work():
73
+ return ActionResult.success(data={"ok": True})
74
+
75
+ await ctx.background_task(_work())
76
+ assert captured["long_running"] is False
@@ -0,0 +1,96 @@
1
+ # tests/test_context_deliver_chat_message.py
2
+ """Component C — ctx.deliver_chat_message() public API surface tests.
3
+
4
+ Verifies the SDK contract:
5
+ - POSTs to auth-gw /v1/internal/chat/inject with correct payload + headers.
6
+ - Truncates text > 64KB with appended marker.
7
+ - Respects msg_type literal values.
8
+ - Forwards optional refresh_panels list.
9
+ """
10
+ from __future__ import annotations
11
+ import pytest
12
+ import httpx
13
+
14
+ from imperal_sdk.context import Context
15
+ from imperal_sdk.types.identity import UserContext
16
+
17
+
18
+ def _make_ctx(*, ext_id="my-ext", user_id="imp_u_abc",
19
+ gw="http://auth-gw:8085", svc="tok_xxx") -> Context:
20
+ ctx = Context(user=UserContext(
21
+ imperal_id=user_id, email="u@t.com", tenant_id="t", role="user",
22
+ ))
23
+ ctx._extension_id = ext_id
24
+ ctx._user_id = user_id # type: ignore[attr-defined]
25
+ ctx._gateway_url = gw
26
+ ctx._service_token = svc
27
+ return ctx
28
+
29
+
30
+ @pytest.mark.asyncio
31
+ async def test_deliver_chat_message_posts_to_auth_gw(monkeypatch):
32
+ captured: dict = {}
33
+
34
+ class _MockAsyncClient:
35
+ def __init__(self, *, timeout, **kw):
36
+ captured["timeout"] = timeout
37
+ async def __aenter__(self): return self
38
+ async def __aexit__(self, *a): pass
39
+ async def post(self, url, json=None, headers=None):
40
+ captured["url"] = url
41
+ captured["json"] = json
42
+ captured["headers"] = headers
43
+ return httpx.Response(200, json={"ok": True})
44
+
45
+ monkeypatch.setattr("imperal_sdk.context.httpx.AsyncClient", _MockAsyncClient)
46
+ ctx = _make_ctx()
47
+
48
+ await ctx.deliver_chat_message("Hello!")
49
+ assert captured["url"].endswith("/v1/internal/chat/inject")
50
+ assert captured["json"]["text"] == "Hello!"
51
+ assert captured["json"]["msg_type"] == "response"
52
+ assert captured["json"]["ext_id"] == "my-ext"
53
+ assert captured["json"]["user_id"] == "imp_u_abc"
54
+ assert captured["headers"]["X-Service-Token"] == "tok_xxx"
55
+ assert captured["headers"]["X-Acting-User"] == "imp_u_abc"
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_deliver_chat_message_truncates_oversized_text(monkeypatch):
60
+ captured: dict = {}
61
+
62
+ class _MockAsyncClient:
63
+ def __init__(self, *, timeout, **kw): pass
64
+ async def __aenter__(self): return self
65
+ async def __aexit__(self, *a): pass
66
+ async def post(self, url, json=None, headers=None):
67
+ captured["text"] = json["text"]
68
+ return httpx.Response(200, json={"ok": True})
69
+
70
+ monkeypatch.setattr("imperal_sdk.context.httpx.AsyncClient", _MockAsyncClient)
71
+ ctx = _make_ctx()
72
+
73
+ big = "x" * 70000
74
+ await ctx.deliver_chat_message(big)
75
+ assert len(captured["text"]) <= 64 * 1024 + len("...(truncated)")
76
+ assert captured["text"].endswith("...(truncated)")
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_deliver_chat_message_with_refresh_panels(monkeypatch):
81
+ captured: dict = {}
82
+
83
+ class _MockAsyncClient:
84
+ def __init__(self, *, timeout, **kw): pass
85
+ async def __aenter__(self): return self
86
+ async def __aexit__(self, *a): pass
87
+ async def post(self, url, json=None, headers=None):
88
+ captured["json"] = json
89
+ return httpx.Response(200, json={"ok": True})
90
+
91
+ monkeypatch.setattr("imperal_sdk.context.httpx.AsyncClient", _MockAsyncClient)
92
+ ctx = _make_ctx()
93
+
94
+ await ctx.deliver_chat_message("Done!", msg_type="tool_result", refresh_panels=["editor"])
95
+ assert captured["json"]["msg_type"] == "tool_result"
96
+ assert captured["json"]["refresh_panels"] == ["editor"]
@@ -0,0 +1,59 @@
1
+ # tests/test_http_timeout_override.py
2
+ import pytest
3
+ import httpx
4
+ from imperal_sdk.http.client import HTTPClient
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_post_accepts_per_call_timeout_override(monkeypatch):
9
+ """Component A: timeout= kwarg overrides instance default."""
10
+ captured: dict = {}
11
+
12
+ class _MockAsyncClient:
13
+ def __init__(self, *, timeout, follow_redirects, max_redirects):
14
+ captured["timeout"] = timeout
15
+ async def __aenter__(self): return self
16
+ async def __aexit__(self, *a): pass
17
+ async def post(self, url, **kw):
18
+ return httpx.Response(200, json={"ok": True})
19
+
20
+ monkeypatch.setattr("imperal_sdk.http.client.httpx.AsyncClient", _MockAsyncClient)
21
+ c = HTTPClient(timeout=30)
22
+ await c.post("https://example.com", timeout=120)
23
+ assert captured["timeout"] == 120
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_post_default_when_no_override(monkeypatch):
28
+ """No timeout= kwarg → uses instance default."""
29
+ captured: dict = {}
30
+
31
+ class _MockAsyncClient:
32
+ def __init__(self, *, timeout, follow_redirects, max_redirects):
33
+ captured["timeout"] = timeout
34
+ async def __aenter__(self): return self
35
+ async def __aexit__(self, *a): pass
36
+ async def post(self, url, **kw):
37
+ return httpx.Response(200, json={"ok": True})
38
+
39
+ monkeypatch.setattr("imperal_sdk.http.client.httpx.AsyncClient", _MockAsyncClient)
40
+ c = HTTPClient(timeout=30)
41
+ await c.post("https://example.com")
42
+ assert captured["timeout"] == 30
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_post_rejects_over_180s_cap():
47
+ """Component A: federal cap 180s — anything more raises ValueError."""
48
+ c = HTTPClient(timeout=30)
49
+ with pytest.raises(ValueError, match="exceeds federal cap"):
50
+ await c.post("https://example.com", timeout=300)
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_post_rejects_zero_or_negative_timeout():
55
+ c = HTTPClient(timeout=30)
56
+ with pytest.raises(ValueError, match="must be > 0"):
57
+ await c.post("https://example.com", timeout=0)
58
+ with pytest.raises(ValueError, match="must be > 0"):
59
+ await c.post("https://example.com", timeout=-5)
@@ -1,51 +0,0 @@
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 httpx
5
-
6
- from imperal_sdk.types.models import HTTPResponse
7
-
8
-
9
- def _wrap(resp: httpx.Response) -> HTTPResponse:
10
- """Convert httpx.Response to typed HTTPResponse."""
11
- content_type = resp.headers.get("content-type", "")
12
- if "json" in content_type:
13
- try:
14
- body = resp.json()
15
- except Exception:
16
- body = resp.text
17
- elif resp.content:
18
- # Return text for text/* or small responses; bytes otherwise
19
- try:
20
- body = resp.text
21
- except Exception:
22
- body = resp.content
23
- else:
24
- body = ""
25
- return HTTPResponse(status_code=resp.status_code, body=body, headers=dict(resp.headers))
26
-
27
-
28
- class HTTPClient:
29
- def __init__(self, timeout: int = 30, max_redirects: int = 5):
30
- self._timeout = timeout
31
- self._max_redirects = max_redirects
32
-
33
- async def get(self, url: str, **kwargs) -> HTTPResponse:
34
- async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
35
- return _wrap(await client.get(url, **kwargs))
36
-
37
- async def post(self, url: str, **kwargs) -> HTTPResponse:
38
- async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
39
- return _wrap(await client.post(url, **kwargs))
40
-
41
- async def put(self, url: str, **kwargs) -> HTTPResponse:
42
- async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
43
- return _wrap(await client.put(url, **kwargs))
44
-
45
- async def patch(self, url: str, **kwargs) -> HTTPResponse:
46
- async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
47
- return _wrap(await client.patch(url, **kwargs))
48
-
49
- async def delete(self, url: str, **kwargs) -> HTTPResponse:
50
- async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
51
- return _wrap(await client.delete(url, **kwargs))
File without changes
File without changes
File without changes