python-corekit 0.3.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. {python_corekit-0.3.0 → python_corekit-0.4.1}/PKG-INFO +56 -4
  2. {python_corekit-0.3.0 → python_corekit-0.4.1}/README.md +53 -2
  3. python_corekit-0.4.1/corekit/exceptionator/__init__.py +68 -0
  4. python_corekit-0.4.1/corekit/exceptionator/actions.py +46 -0
  5. python_corekit-0.4.1/corekit/exceptionator/asgi.py +115 -0
  6. python_corekit-0.4.1/corekit/exceptionator/constants.py +7 -0
  7. python_corekit-0.4.1/corekit/exceptionator/exceptionator.py +62 -0
  8. python_corekit-0.4.1/corekit/exceptionator/fingerprint.py +86 -0
  9. python_corekit-0.4.1/corekit/exceptionator/guard.py +53 -0
  10. python_corekit-0.4.1/corekit/exceptionator/jobs.py +52 -0
  11. python_corekit-0.4.1/corekit/exceptionator/models.py +98 -0
  12. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/__init__.py +5 -0
  13. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/client.py +34 -3
  14. python_corekit-0.4.1/corekit/http/stream.py +110 -0
  15. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/jobs/runner.py +22 -2
  16. python_corekit-0.4.1/corekit/llm/__init__.py +134 -0
  17. python_corekit-0.4.1/corekit/llm/client.py +179 -0
  18. python_corekit-0.4.1/corekit/llm/enum.py +123 -0
  19. python_corekit-0.4.1/corekit/llm/events.py +96 -0
  20. python_corekit-0.4.1/corekit/llm/messages.py +173 -0
  21. python_corekit-0.4.1/corekit/llm/prompts/__init__.py +19 -0
  22. python_corekit-0.4.1/corekit/llm/prompts/enum.py +54 -0
  23. python_corekit-0.4.1/corekit/llm/prompts/exceptions.py +22 -0
  24. python_corekit-0.4.1/corekit/llm/prompts/loader.py +139 -0
  25. python_corekit-0.4.1/corekit/llm/prompts/template.py +53 -0
  26. python_corekit-0.4.1/corekit/llm/protocols.py +65 -0
  27. python_corekit-0.4.1/corekit/llm/streaming.py +149 -0
  28. python_corekit-0.4.1/corekit/llm/tools/__init__.py +19 -0
  29. python_corekit-0.4.1/corekit/llm/tools/base.py +118 -0
  30. python_corekit-0.4.1/corekit/llm/tools/detection.py +99 -0
  31. python_corekit-0.4.1/corekit/llm/tools/loop.py +255 -0
  32. python_corekit-0.4.1/corekit/llm/tools/registry.py +103 -0
  33. python_corekit-0.4.1/corekit/llm/wire.py +199 -0
  34. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/__init__.py +3 -3
  35. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/benchmarkable.py +16 -2
  36. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/timing/split.py +14 -0
  37. python_corekit-0.4.1/corekit/observability/timing/timer.py +54 -0
  38. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/__init__.py +2 -1
  39. python_corekit-0.4.1/corekit/schemas/version.py +58 -0
  40. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/__init__.py +4 -2
  41. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/collections.py +16 -1
  42. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/text.py +21 -2
  43. {python_corekit-0.3.0 → python_corekit-0.4.1}/pyproject.toml +5 -2
  44. {python_corekit-0.3.0 → python_corekit-0.4.1}/python_corekit.egg-info/PKG-INFO +56 -4
  45. {python_corekit-0.3.0 → python_corekit-0.4.1}/python_corekit.egg-info/SOURCES.txt +31 -0
  46. {python_corekit-0.3.0 → python_corekit-0.4.1}/python_corekit.egg-info/requires.txt +1 -0
  47. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_architecture.py +2 -0
  48. python_corekit-0.4.1/tests/test_exceptionator.py +326 -0
  49. python_corekit-0.4.1/tests/test_llm.py +492 -0
  50. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_observability.py +24 -0
  51. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_requests.py +133 -1
  52. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_schemas_enum.py +12 -1
  53. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils.py +15 -0
  54. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils_collections.py +11 -0
  55. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils_text.py +16 -1
  56. python_corekit-0.3.0/corekit/observability/timing/timer.py +0 -32
  57. {python_corekit-0.3.0 → python_corekit-0.4.1}/LICENSE +0 -0
  58. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/__init__.py +0 -0
  59. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/__init__.py +0 -0
  60. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/application.py +0 -0
  61. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/handler.py +0 -0
  62. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/lifespan.py +0 -0
  63. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/middleware.py +0 -0
  64. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/responses.py +0 -0
  65. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/api/routers.py +0 -0
  66. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/concurrency/__init__.py +0 -0
  67. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/concurrency/decorators.py +0 -0
  68. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/concurrency/thread_local.py +0 -0
  69. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/concurrency/worker.py +0 -0
  70. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/config/__init__.py +0 -0
  71. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/config/loader.py +0 -0
  72. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/config/settings.py +0 -0
  73. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/config/sources.py +0 -0
  74. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/__init__.py +0 -0
  75. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/connectable.py +0 -0
  76. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/decorators.py +0 -0
  77. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/redis/__init__.py +0 -0
  78. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/redis/connection.py +0 -0
  79. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/registry.py +0 -0
  80. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/__init__.py +0 -0
  81. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/connection.py +0 -0
  82. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/fields/__init__.py +0 -0
  83. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/fields/jsonb.py +0 -0
  84. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/__init__.py +0 -0
  85. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/base.py +0 -0
  86. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/operations.py +0 -0
  87. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/registry.py +0 -0
  88. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/table.py +0 -0
  89. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/operations/__init__.py +0 -0
  90. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/operations/base.py +0 -0
  91. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/operations/statements.py +0 -0
  92. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/query.py +0 -0
  93. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/connections/sql/table.py +0 -0
  94. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/crypto/__init__.py +0 -0
  95. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/crypto/constants.py +0 -0
  96. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/crypto/enum.py +0 -0
  97. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/crypto/hasher.py +0 -0
  98. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/__init__.py +0 -0
  99. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/dataset.py +0 -0
  100. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/expressions/__init__.py +0 -0
  101. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/expressions/comparison.py +0 -0
  102. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/expressions/expression.py +0 -0
  103. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/expressions/operator.py +0 -0
  104. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/expressions/target.py +0 -0
  105. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/record.py +0 -0
  106. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/data/stats.py +0 -0
  107. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/decorators/__init__.py +0 -0
  108. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/decorators/exception_handling.py +0 -0
  109. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/decorators/warnings.py +0 -0
  110. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/docker/__init__.py +0 -0
  111. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/docker/watchdog.py +0 -0
  112. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/__init__.py +0 -0
  113. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/connection.py +0 -0
  114. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/extract/__init__.py +0 -0
  115. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/extract/extractor.py +0 -0
  116. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/extract/schemas.py +0 -0
  117. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/load/__init__.py +0 -0
  118. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/load/loader.py +0 -0
  119. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/load/schemas.py +0 -0
  120. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/orchestrator.py +0 -0
  121. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/schemas.py +0 -0
  122. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/transform/__init__.py +0 -0
  123. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/transform/schemas.py +0 -0
  124. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/etl/transform/transformer.py +0 -0
  125. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/__init__.py +0 -0
  126. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/enum.py +0 -0
  127. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/frames.py +0 -0
  128. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/models.py +0 -0
  129. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/publisher.py +0 -0
  130. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/reader.py +0 -0
  131. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/sse.py +0 -0
  132. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/events/websocket.py +0 -0
  133. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/exceptions/__init__.py +0 -0
  134. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/exceptions/base.py +0 -0
  135. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/exceptions/custom/__init__.py +0 -0
  136. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/exceptions/enum.py +0 -0
  137. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/exceptions/http/__init__.py +0 -0
  138. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/exceptions/types.py +0 -0
  139. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/files/__init__.py +0 -0
  140. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/files/base.py +0 -0
  141. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/files/enum.py +0 -0
  142. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/files/json.py +0 -0
  143. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/files/toml.py +0 -0
  144. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/api.py +0 -0
  145. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/exceptions.py +0 -0
  146. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/exponential_backoff.py +0 -0
  147. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/response.py +0 -0
  148. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/http/status.py +0 -0
  149. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/jobs/__init__.py +0 -0
  150. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/jobs/registry.py +0 -0
  151. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/jobs/task.py +0 -0
  152. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/log_monitor/__init__.py +0 -0
  153. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/log_monitor/constants.py +0 -0
  154. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/log_monitor/models.py +0 -0
  155. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/log_monitor/service.py +0 -0
  156. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/notifications/__init__.py +0 -0
  157. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/notifications/base.py +0 -0
  158. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/notifications/models.py +0 -0
  159. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/loggable.py +0 -0
  160. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/request_context.py +0 -0
  161. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/timing/__init__.py +0 -0
  162. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/observability/timing/constants.py +0 -0
  163. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/py.typed +0 -0
  164. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/registry/__init__.py +0 -0
  165. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/registry/ordered.py +0 -0
  166. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/registry/registry.py +0 -0
  167. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/dataclasses/__init__.py +0 -0
  168. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/enum.py +0 -0
  169. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/models/__init__.py +0 -0
  170. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/models/arbitrary.py +0 -0
  171. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/models/date_models.py +0 -0
  172. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/pydantic/__init__.py +0 -0
  173. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/pydantic/fields.py +0 -0
  174. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/schemas/types.py +0 -0
  175. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/serialization/__init__.py +0 -0
  176. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/serialization/enum.py +0 -0
  177. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/serialization/pickle_file.py +0 -0
  178. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/serialization/serializable.py +0 -0
  179. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/serialization/serializer.py +0 -0
  180. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/coercion.py +0 -0
  181. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/ids.py +0 -0
  182. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/payload.py +0 -0
  183. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/raise_exc.py +0 -0
  184. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/time.py +0 -0
  185. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/validators.py +0 -0
  186. {python_corekit-0.3.0 → python_corekit-0.4.1}/corekit/utils/void.py +0 -0
  187. {python_corekit-0.3.0 → python_corekit-0.4.1}/python_corekit.egg-info/dependency_links.txt +0 -0
  188. {python_corekit-0.3.0 → python_corekit-0.4.1}/python_corekit.egg-info/top_level.txt +0 -0
  189. {python_corekit-0.3.0 → python_corekit-0.4.1}/setup.cfg +0 -0
  190. {python_corekit-0.3.0 → python_corekit-0.4.1}/setup.py +0 -0
  191. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_api.py +0 -0
  192. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_application.py +0 -0
  193. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_concurrency.py +0 -0
  194. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_config.py +0 -0
  195. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_connections.py +0 -0
  196. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_data.py +0 -0
  197. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_docker.py +0 -0
  198. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_etl.py +0 -0
  199. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_events.py +0 -0
  200. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_exceptions.py +0 -0
  201. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_expressions.py +0 -0
  202. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_expressions_sqlalchemy.py +0 -0
  203. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_files.py +0 -0
  204. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_imports.py +0 -0
  205. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_imports_are_top_level.py +0 -0
  206. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_jobs.py +0 -0
  207. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_lifespan.py +0 -0
  208. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_log_monitor.py +0 -0
  209. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_middleware_stack.py +0 -0
  210. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_migration.py +0 -0
  211. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_notifications.py +0 -0
  212. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_operations.py +0 -0
  213. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_ordered_registry.py +0 -0
  214. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_redis.py +0 -0
  215. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_registry.py +0 -0
  216. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_request_context.py +0 -0
  217. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_serialization.py +0 -0
  218. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_sql.py +0 -0
  219. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils_coercion.py +0 -0
  220. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils_ids.py +0 -0
  221. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils_payload.py +0 -0
  222. {python_corekit-0.3.0 → python_corekit-0.4.1}/tests/test_utils_time.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-corekit
3
- Version: 0.3.0
4
- Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, and ETL
3
+ Version: 0.4.1
4
+ Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, ETL, and LLM chat/tool primitives
5
5
  Author: Steven Jacobsen
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/stevejaker/corekit
@@ -18,6 +18,7 @@ License-File: LICENSE
18
18
  Requires-Dist: pydantic<3,>=2.10
19
19
  Requires-Dist: pydantic-settings<3,>=2.0
20
20
  Requires-Dist: fastapi<1,>=0.115
21
+ Requires-Dist: starlette<0.47,>=0.40
21
22
  Requires-Dist: sqlmodel<0.1,>=0.0.16
22
23
  Requires-Dist: SQLAlchemy<3,>=2.0
23
24
  Requires-Dist: redis<7,>=5.0
@@ -37,7 +38,8 @@ Dynamic: license-file
37
38
 
38
39
  Shared foundations for Python projects: structured logging, benchmarking,
39
40
  registries, a FastAPI application with routers and handlers, SQL statements and
40
- migrations, background tasks, an in-memory record store, and ETL scaffolding.
41
+ migrations, background tasks, an in-memory record store, ETL scaffolding, and
42
+ OpenAI-shaped chat / tool-loop primitives.
41
43
 
42
44
  Requires Python 3.11+.
43
45
 
@@ -55,7 +57,7 @@ extras to remember, and no import that fails because something was left out.
55
57
  Pin a compatible release rather than tracking whatever is newest:
56
58
 
57
59
  ```
58
- python-corekit~=0.3.0
60
+ python-corekit~=0.4.0
59
61
  ```
60
62
 
61
63
  Before 1.0, the minor version carries breaking changes.
@@ -88,6 +90,29 @@ class Report(Benchmarkable):
88
90
  self.timing("queried") # logs the time since the previous split
89
91
  ```
90
92
 
93
+ ## Exceptionator
94
+
95
+ Catch an exception, run one action, then rethrow so the existing failure path
96
+ still happens. Built-in actions are no-op and log-traceback; apps supply their
97
+ own (persist, notify, …).
98
+
99
+ ```python
100
+ from corekit.exceptionator import (
101
+ Exceptionator,
102
+ ExceptionatorMiddleware,
103
+ LogTracebackAction,
104
+ install_run_task_hook,
105
+ set_process_exceptionator,
106
+ )
107
+
108
+ exceptionator = Exceptionator(LogTracebackAction(), service="api")
109
+ set_process_exceptionator(exceptionator)
110
+ install_run_task_hook(exceptionator) # optional; run_task captures with task name
111
+ ```
112
+
113
+ `ExceptionatorMiddleware` is raw ASGI (not `BaseHTTPMiddleware`). With no
114
+ `run_task` hook installed, jobs behave as in 0.4.0.
115
+
91
116
  ## Assembling an application
92
117
 
93
118
  `Application` is a plain `FastAPI` subclass — every constructor argument,
@@ -281,6 +306,32 @@ ceiling, not 9,999 threads.
281
306
  Failures propagate by default. Pass `raise_on_error=False` to log and skip them
282
307
  instead, which loses results silently and so is opt-in.
283
308
 
309
+ ## LLM chat and tools
310
+
311
+ OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
312
+ ``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
313
+ server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
314
+ facade.
315
+
316
+ ```python
317
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
318
+
319
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
320
+ prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
321
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
322
+
323
+ registry = ToolRegistry()
324
+ registry.register(MyTool())
325
+ loop = ToolLoop(client, registry)
326
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
327
+ ...
328
+ ```
329
+
330
+ ``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
331
+ (``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
332
+ *text* stays with the consumer — corekit only ships the loader. See
333
+ `docs/LLM_EXTRACTION.md`.
334
+
284
335
  ## HTTP clients
285
336
 
286
337
  ```python
@@ -394,6 +445,7 @@ corekit/
394
445
 
395
446
  api/ Application, lifespan, middleware, routers
396
447
  docker/ notifications/ etl/
448
+ llm/ messages, tools, agentic loop (no GenerationClient)
397
449
 
398
450
  events/ log_monitor/ built on the capabilities above
399
451
  ```
@@ -2,7 +2,8 @@
2
2
 
3
3
  Shared foundations for Python projects: structured logging, benchmarking,
4
4
  registries, a FastAPI application with routers and handlers, SQL statements and
5
- migrations, background tasks, an in-memory record store, and ETL scaffolding.
5
+ migrations, background tasks, an in-memory record store, ETL scaffolding, and
6
+ OpenAI-shaped chat / tool-loop primitives.
6
7
 
7
8
  Requires Python 3.11+.
8
9
 
@@ -20,7 +21,7 @@ extras to remember, and no import that fails because something was left out.
20
21
  Pin a compatible release rather than tracking whatever is newest:
21
22
 
22
23
  ```
23
- python-corekit~=0.3.0
24
+ python-corekit~=0.4.0
24
25
  ```
25
26
 
26
27
  Before 1.0, the minor version carries breaking changes.
@@ -53,6 +54,29 @@ class Report(Benchmarkable):
53
54
  self.timing("queried") # logs the time since the previous split
54
55
  ```
55
56
 
57
+ ## Exceptionator
58
+
59
+ Catch an exception, run one action, then rethrow so the existing failure path
60
+ still happens. Built-in actions are no-op and log-traceback; apps supply their
61
+ own (persist, notify, …).
62
+
63
+ ```python
64
+ from corekit.exceptionator import (
65
+ Exceptionator,
66
+ ExceptionatorMiddleware,
67
+ LogTracebackAction,
68
+ install_run_task_hook,
69
+ set_process_exceptionator,
70
+ )
71
+
72
+ exceptionator = Exceptionator(LogTracebackAction(), service="api")
73
+ set_process_exceptionator(exceptionator)
74
+ install_run_task_hook(exceptionator) # optional; run_task captures with task name
75
+ ```
76
+
77
+ `ExceptionatorMiddleware` is raw ASGI (not `BaseHTTPMiddleware`). With no
78
+ `run_task` hook installed, jobs behave as in 0.4.0.
79
+
56
80
  ## Assembling an application
57
81
 
58
82
  `Application` is a plain `FastAPI` subclass — every constructor argument,
@@ -246,6 +270,32 @@ ceiling, not 9,999 threads.
246
270
  Failures propagate by default. Pass `raise_on_error=False` to log and skip them
247
271
  instead, which loses results silently and so is opt-in.
248
272
 
273
+ ## LLM chat and tools
274
+
275
+ OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
276
+ ``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
277
+ server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
278
+ facade.
279
+
280
+ ```python
281
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
282
+
283
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
284
+ prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
285
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
286
+
287
+ registry = ToolRegistry()
288
+ registry.register(MyTool())
289
+ loop = ToolLoop(client, registry)
290
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
291
+ ...
292
+ ```
293
+
294
+ ``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
295
+ (``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
296
+ *text* stays with the consumer — corekit only ships the loader. See
297
+ `docs/LLM_EXTRACTION.md`.
298
+
249
299
  ## HTTP clients
250
300
 
251
301
  ```python
@@ -359,6 +409,7 @@ corekit/
359
409
 
360
410
  api/ Application, lifespan, middleware, routers
361
411
  docker/ notifications/ etl/
412
+ llm/ messages, tools, agentic loop (no GenerationClient)
362
413
 
363
414
  events/ log_monitor/ built on the capabilities above
364
415
  ```
@@ -0,0 +1,68 @@
1
+ """
2
+ Process-wide exception capture: catch, run one action, rethrow.
3
+
4
+ Product concerns (Postgres, ntfy, prompts) stay in the installing application.
5
+ This package owns the wrapper, built-in actions, fingerprinting, ASGI middleware,
6
+ and the ``run_task`` hook point.
7
+ """
8
+
9
+ from corekit.exceptionator.actions import (
10
+ ExceptionatorAction,
11
+ LogTracebackAction,
12
+ NoOpAction,
13
+ )
14
+ from corekit.exceptionator.asgi import (
15
+ ExceptionatorMiddleware,
16
+ get_process_exceptionator,
17
+ record_http_exception,
18
+ set_process_exceptionator,
19
+ should_record_http_exception,
20
+ )
21
+ from corekit.exceptionator.exceptionator import Exceptionator
22
+ from corekit.exceptionator.fingerprint import ExceptionFingerprint, ExceptionText
23
+ from corekit.exceptionator.guard import (
24
+ begin_handling,
25
+ end_handling,
26
+ handling_active,
27
+ is_captured,
28
+ mark_captured,
29
+ )
30
+ from corekit.exceptionator.jobs import (
31
+ clear_run_task_hook,
32
+ get_run_task_hook,
33
+ install_run_task_hook,
34
+ )
35
+ from corekit.exceptionator.models import (
36
+ CapturedException,
37
+ ExceptionatorActionName,
38
+ ExceptionContext,
39
+ ExceptionSource,
40
+ build_captured,
41
+ )
42
+
43
+ __all__ = [
44
+ "CapturedException",
45
+ "ExceptionContext",
46
+ "ExceptionFingerprint",
47
+ "ExceptionSource",
48
+ "ExceptionText",
49
+ "Exceptionator",
50
+ "ExceptionatorAction",
51
+ "ExceptionatorActionName",
52
+ "ExceptionatorMiddleware",
53
+ "LogTracebackAction",
54
+ "NoOpAction",
55
+ "begin_handling",
56
+ "build_captured",
57
+ "clear_run_task_hook",
58
+ "end_handling",
59
+ "get_process_exceptionator",
60
+ "get_run_task_hook",
61
+ "handling_active",
62
+ "install_run_task_hook",
63
+ "is_captured",
64
+ "mark_captured",
65
+ "record_http_exception",
66
+ "set_process_exceptionator",
67
+ "should_record_http_exception",
68
+ ]
@@ -0,0 +1,46 @@
1
+ """
2
+ Built-in Exceptionator actions.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from corekit.exceptionator.models import CapturedException
8
+ from corekit.observability.loggable import Loggable
9
+
10
+ __all__ = ["ExceptionatorAction", "LogTracebackAction", "NoOpAction"]
11
+
12
+
13
+ class ExceptionatorAction(Loggable):
14
+ """
15
+ Work performed after an exception is captured and before it continues.
16
+ """
17
+
18
+ def run(self, captured: CapturedException) -> None:
19
+ """
20
+ Perform the action. Must not raise.
21
+ """
22
+ raise NotImplementedError
23
+
24
+
25
+ class NoOpAction(ExceptionatorAction):
26
+ """
27
+ Catch and continue. Records nothing and publishes nothing.
28
+ """
29
+
30
+ def run(self, captured: CapturedException) -> None:
31
+ """
32
+ Do nothing with the captured exception.
33
+ """
34
+ return None
35
+
36
+
37
+ class LogTracebackAction(ExceptionatorAction):
38
+ """
39
+ Write the message and traceback. Does not store or notify.
40
+ """
41
+
42
+ def run(self, captured: CapturedException) -> None:
43
+ """
44
+ Log the incident text.
45
+ """
46
+ self.error(f"{captured.service} {captured.exception_type}: {captured.message}\n{captured.traceback_text}")
@@ -0,0 +1,115 @@
1
+ """
2
+ ASGI middleware and HTTP helpers for Exceptionator.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Awaitable, Callable
8
+ from typing import Any
9
+
10
+ from fastapi import Request
11
+ from fastapi.exceptions import RequestValidationError
12
+ from fastapi.responses import JSONResponse
13
+ from starlette.exceptions import HTTPException
14
+ from starlette.websockets import WebSocketDisconnect
15
+
16
+ from corekit.exceptionator.exceptionator import Exceptionator
17
+ from corekit.exceptionator.models import ExceptionContext, ExceptionSource
18
+
19
+ __all__ = [
20
+ "ExceptionatorMiddleware",
21
+ "get_process_exceptionator",
22
+ "record_http_exception",
23
+ "set_process_exceptionator",
24
+ "should_record_http_exception",
25
+ ]
26
+
27
+ ASGIApp = Callable[..., Awaitable[None]]
28
+
29
+ _process_exceptionator: Exceptionator | None = None
30
+
31
+
32
+ def set_process_exceptionator(exceptionator: Exceptionator) -> None:
33
+ """
34
+ Remember the process Exceptionator for middleware and HTTP handlers.
35
+ """
36
+ global _process_exceptionator
37
+ _process_exceptionator = exceptionator
38
+
39
+
40
+ def get_process_exceptionator() -> Exceptionator:
41
+ """
42
+ The Exceptionator installed for this process.
43
+ """
44
+ if _process_exceptionator is None:
45
+ raise RuntimeError("No Exceptionator installed; call set_process_exceptionator first")
46
+ return _process_exceptionator
47
+
48
+
49
+ def should_record_http_exception(exc: BaseException) -> bool:
50
+ """
51
+ Client errors and validation failures are not incidents. HTTP 500s are.
52
+ """
53
+ if isinstance(exc, RequestValidationError):
54
+ return False
55
+ if isinstance(exc, WebSocketDisconnect):
56
+ return False
57
+ status = getattr(exc, "status_code", None)
58
+ if isinstance(exc, HTTPException) and isinstance(status, int) and status < 500:
59
+ return False
60
+ return True
61
+
62
+
63
+ class ExceptionatorMiddleware:
64
+ """
65
+ ASGI middleware that records unhandled HTTP failures and rethrows them.
66
+
67
+ Not ``BaseHTTPMiddleware``: that wrapper breaks streaming and exception groups.
68
+ """
69
+
70
+ def __init__(self, app: ASGIApp, exceptionator: Exceptionator | None = None) -> None:
71
+ self.app = app
72
+ self._exceptionator = exceptionator
73
+
74
+ async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
75
+ if scope.get("type") != "http":
76
+ await self.app(scope, receive, send)
77
+ return
78
+ try:
79
+ await self.app(scope, receive, send)
80
+ except Exception as exc:
81
+ if should_record_http_exception(exc):
82
+ self._resolve().capture(
83
+ exc,
84
+ context=ExceptionContext(
85
+ source=ExceptionSource.HTTP,
86
+ http_method=scope.get("method"),
87
+ http_path=scope.get("path"),
88
+ ),
89
+ )
90
+ raise
91
+
92
+ def _resolve(self) -> Exceptionator:
93
+ if self._exceptionator is not None:
94
+ return self._exceptionator
95
+ return get_process_exceptionator()
96
+
97
+
98
+ def record_http_exception(request: Request, exc: HTTPException) -> JSONResponse:
99
+ """
100
+ Store HTTP 500s, then return the same JSON body FastAPI would have sent.
101
+
102
+ Starlette answers ``HTTPException`` inside the router, so the ASGI middleware
103
+ never sees it. 4xx responses are returned unchanged and are not incidents.
104
+ """
105
+ if should_record_http_exception(exc):
106
+ get_process_exceptionator().capture(
107
+ exc,
108
+ context=ExceptionContext(
109
+ source=ExceptionSource.HTTP,
110
+ http_method=request.method,
111
+ http_path=request.url.path,
112
+ ),
113
+ )
114
+ headers = getattr(exc, "headers", None)
115
+ return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}, headers=headers)
@@ -0,0 +1,7 @@
1
+ """
2
+ Limits shared by every Exceptionator install.
3
+ """
4
+
5
+ MESSAGE_MAX_CHARS = 2_000
6
+ TRACEBACK_MAX_CHARS = 32_000
7
+ FINGERPRINT_FRAMES = 8
@@ -0,0 +1,62 @@
1
+ """
2
+ Catch an exception, run one action, and let the caller rethrow.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Iterator
8
+ from contextlib import contextmanager
9
+
10
+ from corekit.exceptionator.actions import ExceptionatorAction
11
+ from corekit.exceptionator.guard import (
12
+ begin_handling,
13
+ end_handling,
14
+ handling_active,
15
+ is_captured,
16
+ mark_captured,
17
+ )
18
+ from corekit.exceptionator.models import CapturedException, ExceptionContext, build_captured
19
+ from corekit.observability.loggable import Loggable
20
+
21
+ __all__ = ["Exceptionator"]
22
+
23
+
24
+ class Exceptionator(Loggable):
25
+ """
26
+ Catch an exception, run the configured action, and let the caller rethrow.
27
+
28
+ ``guard`` rethrows. ``capture`` runs the same action and returns, for a
29
+ caller that has already decided to swallow the error.
30
+ """
31
+
32
+ def __init__(self, action: ExceptionatorAction, *, service: str) -> None:
33
+ super().__init__()
34
+ self._action = action
35
+ self.service = service
36
+
37
+ @contextmanager
38
+ def guard(self, context: ExceptionContext | None = None) -> Iterator[None]:
39
+ """
40
+ Run the action when the block raises, then rethrow the original exception.
41
+ """
42
+ try:
43
+ yield
44
+ except Exception as exc:
45
+ self.capture(exc, context=context)
46
+ raise
47
+
48
+ def capture(self, exc: BaseException, *, context: ExceptionContext | None = None) -> None:
49
+ """
50
+ Run the action once for this exception. Later calls on the same object no-op.
51
+ """
52
+ if handling_active() or is_captured(exc):
53
+ return
54
+ token = begin_handling()
55
+ try:
56
+ captured: CapturedException = build_captured(exc, service=self.service, context=context)
57
+ mark_captured(exc)
58
+ self._action.run(captured)
59
+ except Exception:
60
+ self.exception("Exceptionator failed while recording an exception")
61
+ finally:
62
+ end_handling(token)
@@ -0,0 +1,86 @@
1
+ """
2
+ Identity for an exception based on its type and innermost frames.
3
+
4
+ The message is left out so ids embedded in the text do not split one bug into
5
+ many fingerprints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import traceback
12
+ from types import TracebackType
13
+ from typing import ClassVar
14
+
15
+ from corekit.exceptionator.constants import FINGERPRINT_FRAMES
16
+ from corekit.utils.text import truncate
17
+
18
+ __all__ = ["ExceptionFingerprint", "ExceptionText"]
19
+
20
+
21
+ class ExceptionText:
22
+ """
23
+ Backend text extracted from an exception.
24
+ """
25
+
26
+ @staticmethod
27
+ def message(exc: BaseException) -> str:
28
+ """
29
+ Backend message. Public exceptions stringify to user copy, so prefer ``for_log``.
30
+ """
31
+ for_log = getattr(exc, "for_log", None)
32
+ if callable(for_log):
33
+ try:
34
+ return str(for_log())
35
+ except Exception:
36
+ return str(exc)
37
+ return str(exc)
38
+
39
+ @staticmethod
40
+ def first_line(text: str, limit: int) -> str:
41
+ """
42
+ The first line of ``text``, truncated to ``limit`` characters.
43
+ """
44
+ line = text.splitlines()[0] if text else ""
45
+ return truncate(line, limit)
46
+
47
+
48
+ class ExceptionFingerprint:
49
+ """
50
+ Stable identity for an exception based on its type and innermost frames.
51
+ """
52
+
53
+ path_markers: ClassVar[tuple[str, ...]] = ()
54
+
55
+ def of(self, exc: BaseException) -> str:
56
+ """
57
+ Hash the exception type plus the innermost stack frames.
58
+ """
59
+ frames = traceback.extract_tb(exc.__traceback__)[-FINGERPRINT_FRAMES:]
60
+ parts = [type(exc).__qualname__]
61
+ for frame in frames:
62
+ parts.append(f"{self.normalize_path(frame.filename)}|{frame.name}|{frame.lineno}")
63
+ return hashlib.sha256("\n".join(parts).encode()).hexdigest()
64
+
65
+ @classmethod
66
+ def normalize_path(cls, filename: str) -> str:
67
+ """
68
+ Drop machine-specific prefixes so a container path and a checkout match.
69
+ """
70
+ normalized = filename.replace("\\", "/")
71
+ for marker in cls.path_markers:
72
+ index = normalized.find(marker)
73
+ if index >= 0:
74
+ return normalized[index:]
75
+ parts = [part for part in normalized.split("/") if part]
76
+ if not parts:
77
+ return normalized
78
+ return "/".join(parts[-2:])
79
+
80
+ @staticmethod
81
+ def format_traceback(exc: BaseException) -> str:
82
+ """
83
+ Render the traceback text for storage.
84
+ """
85
+ tb: TracebackType | None = exc.__traceback__
86
+ return "".join(traceback.format_exception(type(exc), exc, tb))
@@ -0,0 +1,53 @@
1
+ """
2
+ ContextVar and per-exception marker so one failure is recorded once.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from contextvars import ContextVar, Token
8
+
9
+ __all__ = [
10
+ "begin_handling",
11
+ "end_handling",
12
+ "handling_active",
13
+ "is_captured",
14
+ "mark_captured",
15
+ ]
16
+
17
+ CAPTURED_MARKER = "_exceptionator_captured"
18
+ _handling: ContextVar[bool] = ContextVar("exceptionator_handling", default=False)
19
+
20
+
21
+ def is_captured(exc: BaseException) -> bool:
22
+ """
23
+ Whether this exception object was already handed to an Exceptionator.
24
+ """
25
+ return bool(getattr(exc, CAPTURED_MARKER, False))
26
+
27
+
28
+ def mark_captured(exc: BaseException) -> None:
29
+ """
30
+ Remember that this exception object has been handled.
31
+ """
32
+ setattr(exc, CAPTURED_MARKER, True)
33
+
34
+
35
+ def handling_active() -> bool:
36
+ """
37
+ Whether this task is already inside the Exceptionator.
38
+ """
39
+ return _handling.get()
40
+
41
+
42
+ def begin_handling() -> Token[bool]:
43
+ """
44
+ Mark this task as inside the Exceptionator. Pair with ``end_handling``.
45
+ """
46
+ return _handling.set(True)
47
+
48
+
49
+ def end_handling(token: Token[bool]) -> None:
50
+ """
51
+ Leave the Exceptionator guard.
52
+ """
53
+ _handling.reset(token)
@@ -0,0 +1,52 @@
1
+ """
2
+ Hook ``run_task`` calls after a task fails and before ``failure()``.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING
9
+
10
+ from corekit.exceptionator.models import ExceptionContext, ExceptionSource
11
+
12
+ if TYPE_CHECKING:
13
+ from corekit.exceptionator.exceptionator import Exceptionator
14
+
15
+ __all__ = ["clear_run_task_hook", "get_run_task_hook", "install_run_task_hook"]
16
+
17
+ RunTaskHook = Callable[[str, BaseException], None]
18
+
19
+ _hook: RunTaskHook | None = None
20
+
21
+
22
+ def install_run_task_hook(exceptionator: Exceptionator) -> None:
23
+ """
24
+ Register ``exceptionator`` so ``run_task`` captures failures with the task name.
25
+
26
+ Passing the same instance again is a no-op. Call ``clear_run_task_hook`` to
27
+ remove it. With no hook registered, ``run_task`` behaves as it did before.
28
+ """
29
+ global _hook
30
+
31
+ def hook(task_name: str, exc: BaseException) -> None:
32
+ exceptionator.capture(
33
+ exc,
34
+ context=ExceptionContext(source=ExceptionSource.RQ, job_name=task_name),
35
+ )
36
+
37
+ _hook = hook
38
+
39
+
40
+ def clear_run_task_hook() -> None:
41
+ """
42
+ Remove any ``run_task`` capture hook.
43
+ """
44
+ global _hook
45
+ _hook = None
46
+
47
+
48
+ def get_run_task_hook() -> RunTaskHook | None:
49
+ """
50
+ The hook ``run_task`` should call, or ``None``.
51
+ """
52
+ return _hook