python-corekit 0.2.0__tar.gz → 0.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 (229) hide show
  1. {python_corekit-0.2.0/python_corekit.egg-info → python_corekit-0.4.0}/PKG-INFO +38 -9
  2. {python_corekit-0.2.0 → python_corekit-0.4.0}/README.md +35 -7
  3. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/application.py +47 -9
  4. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/lifespan.py +26 -3
  5. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/concurrency/__init__.py +2 -2
  6. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/concurrency/decorators.py +32 -5
  7. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/concurrency/thread_local.py +2 -2
  8. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/concurrency/worker.py +9 -0
  9. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/config/loader.py +42 -5
  10. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/config/settings.py +11 -1
  11. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/__init__.py +7 -1
  12. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/connectable.py +45 -4
  13. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/redis/connection.py +53 -10
  14. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/__init__.py +2 -1
  15. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/connection.py +39 -5
  16. python_corekit-0.4.0/corekit/connections/sql/fields/__init__.py +7 -0
  17. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/fields/jsonb.py +13 -6
  18. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/__init__.py +4 -0
  19. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/operations.py +69 -2
  20. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/base.py +11 -2
  21. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/statements.py +25 -5
  22. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/table.py +7 -29
  23. python_corekit-0.4.0/corekit/crypto/__init__.py +3 -0
  24. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/crypto/constants.py +2 -2
  25. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/crypto/hasher.py +9 -4
  26. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/dataset.py +8 -2
  27. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/expressions/__init__.py +3 -3
  28. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/expressions/comparison.py +19 -80
  29. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/expressions/expression.py +0 -32
  30. python_corekit-0.4.0/corekit/data/expressions/operator.py +39 -0
  31. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/stats.py +3 -0
  32. python_corekit-0.4.0/corekit/decorators/exception_handling.py +72 -0
  33. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/docker/watchdog.py +50 -31
  34. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/__init__.py +2 -1
  35. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/connection.py +14 -12
  36. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/extract/extractor.py +6 -13
  37. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/orchestrator.py +19 -2
  38. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/schemas.py +2 -2
  39. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/transform/transformer.py +4 -1
  40. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/publisher.py +1 -1
  41. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/reader.py +26 -21
  42. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/sse.py +4 -1
  43. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/websocket.py +24 -11
  44. python_corekit-0.4.0/corekit/exceptions/__init__.py +33 -0
  45. python_corekit-0.4.0/corekit/exceptions/base.py +174 -0
  46. python_corekit-0.4.0/corekit/exceptions/enum.py +17 -0
  47. python_corekit-0.4.0/corekit/exceptions/types.py +17 -0
  48. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/files/__init__.py +2 -4
  49. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/files/base.py +15 -2
  50. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/files/enum.py +0 -5
  51. python_corekit-0.4.0/corekit/files/json.py +26 -0
  52. python_corekit-0.4.0/corekit/http/__init__.py +56 -0
  53. python_corekit-0.4.0/corekit/http/api.py +24 -0
  54. python_corekit-0.4.0/corekit/http/client.py +234 -0
  55. python_corekit-0.4.0/corekit/http/exceptions.py +140 -0
  56. python_corekit-0.4.0/corekit/http/response.py +61 -0
  57. python_corekit-0.4.0/corekit/http/status.py +89 -0
  58. python_corekit-0.4.0/corekit/http/stream.py +110 -0
  59. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/jobs/runner.py +12 -1
  60. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/jobs/task.py +23 -2
  61. python_corekit-0.4.0/corekit/llm/__init__.py +134 -0
  62. python_corekit-0.4.0/corekit/llm/client.py +179 -0
  63. python_corekit-0.4.0/corekit/llm/enum.py +123 -0
  64. python_corekit-0.4.0/corekit/llm/events.py +96 -0
  65. python_corekit-0.4.0/corekit/llm/messages.py +173 -0
  66. python_corekit-0.4.0/corekit/llm/prompts/__init__.py +19 -0
  67. python_corekit-0.4.0/corekit/llm/prompts/enum.py +54 -0
  68. python_corekit-0.4.0/corekit/llm/prompts/exceptions.py +22 -0
  69. python_corekit-0.4.0/corekit/llm/prompts/loader.py +139 -0
  70. python_corekit-0.4.0/corekit/llm/prompts/template.py +53 -0
  71. python_corekit-0.4.0/corekit/llm/protocols.py +65 -0
  72. python_corekit-0.4.0/corekit/llm/streaming.py +149 -0
  73. python_corekit-0.4.0/corekit/llm/tools/__init__.py +19 -0
  74. python_corekit-0.4.0/corekit/llm/tools/base.py +118 -0
  75. python_corekit-0.4.0/corekit/llm/tools/detection.py +99 -0
  76. python_corekit-0.4.0/corekit/llm/tools/loop.py +255 -0
  77. python_corekit-0.4.0/corekit/llm/tools/registry.py +103 -0
  78. python_corekit-0.4.0/corekit/llm/wire.py +199 -0
  79. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/log_monitor/models.py +8 -2
  80. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/log_monitor/service.py +77 -38
  81. python_corekit-0.4.0/corekit/notifications/base.py +59 -0
  82. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/observability/__init__.py +12 -5
  83. python_corekit-0.4.0/corekit/observability/benchmarkable.py +44 -0
  84. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/observability/loggable.py +21 -0
  85. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/observability/request_context.py +55 -2
  86. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/observability/timing/split.py +14 -0
  87. python_corekit-0.4.0/corekit/observability/timing/timer.py +54 -0
  88. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/registry/__init__.py +2 -2
  89. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/registry/registry.py +55 -14
  90. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/__init__.py +2 -1
  91. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/enum.py +22 -1
  92. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/types.py +6 -1
  93. python_corekit-0.4.0/corekit/schemas/version.py +58 -0
  94. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/serialization/__init__.py +2 -0
  95. python_corekit-0.4.0/corekit/serialization/pickle_file.py +61 -0
  96. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/serialization/serializable.py +22 -2
  97. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/serialization/serializer.py +9 -2
  98. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/__init__.py +2 -1
  99. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/collections.py +38 -14
  100. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/payload.py +12 -0
  101. {python_corekit-0.2.0 → python_corekit-0.4.0}/pyproject.toml +6 -3
  102. {python_corekit-0.2.0 → python_corekit-0.4.0/python_corekit.egg-info}/PKG-INFO +38 -9
  103. {python_corekit-0.2.0 → python_corekit-0.4.0}/python_corekit.egg-info/SOURCES.txt +30 -3
  104. {python_corekit-0.2.0 → python_corekit-0.4.0}/python_corekit.egg-info/requires.txt +1 -0
  105. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_application.py +46 -0
  106. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_architecture.py +6 -1
  107. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_config.py +6 -5
  108. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_connections.py +26 -0
  109. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_data.py +10 -0
  110. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_docker.py +13 -2
  111. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_etl.py +24 -1
  112. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_events.py +67 -6
  113. python_corekit-0.4.0/tests/test_exceptions.py +90 -0
  114. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_expressions.py +10 -88
  115. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_expressions_sqlalchemy.py +26 -3
  116. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_files.py +23 -3
  117. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_jobs.py +32 -0
  118. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_lifespan.py +14 -0
  119. python_corekit-0.4.0/tests/test_llm.py +492 -0
  120. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_log_monitor.py +115 -0
  121. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_migration.py +42 -0
  122. python_corekit-0.4.0/tests/test_notifications.py +40 -0
  123. python_corekit-0.4.0/tests/test_observability.py +75 -0
  124. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_operations.py +36 -0
  125. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_redis.py +47 -4
  126. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_registry.py +34 -1
  127. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_request_context.py +33 -1
  128. python_corekit-0.4.0/tests/test_requests.py +389 -0
  129. python_corekit-0.4.0/tests/test_schemas_enum.py +47 -0
  130. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_serialization.py +25 -0
  131. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_sql.py +45 -1
  132. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_utils.py +39 -1
  133. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_utils_collections.py +55 -0
  134. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_utils_payload.py +16 -0
  135. python_corekit-0.2.0/corekit/connections/sql/fields/__init__.py +0 -7
  136. python_corekit-0.2.0/corekit/constants.py +0 -45
  137. python_corekit-0.2.0/corekit/crypto/__init__.py +0 -1
  138. python_corekit-0.2.0/corekit/data/expressions/operator.py +0 -54
  139. python_corekit-0.2.0/corekit/decorators/exception_handling.py +0 -44
  140. python_corekit-0.2.0/corekit/exceptions/__init__.py +0 -18
  141. python_corekit-0.2.0/corekit/exceptions/base.py +0 -45
  142. python_corekit-0.2.0/corekit/exceptions/http/exceptions.py +0 -37
  143. python_corekit-0.2.0/corekit/exceptions/types.py +0 -17
  144. python_corekit-0.2.0/corekit/files/json.py +0 -12
  145. python_corekit-0.2.0/corekit/files/pickle.py +0 -12
  146. python_corekit-0.2.0/corekit/http/__init__.py +0 -13
  147. python_corekit-0.2.0/corekit/http/client.py +0 -176
  148. python_corekit-0.2.0/corekit/http/response.py +0 -12
  149. python_corekit-0.2.0/corekit/notifications/base.py +0 -51
  150. python_corekit-0.2.0/corekit/observability/benchmarkable.py +0 -12
  151. python_corekit-0.2.0/corekit/observability/timing/timer.py +0 -30
  152. python_corekit-0.2.0/tests/test_requests.py +0 -204
  153. {python_corekit-0.2.0 → python_corekit-0.4.0}/LICENSE +0 -0
  154. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/__init__.py +0 -0
  155. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/__init__.py +0 -0
  156. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/handler.py +0 -0
  157. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/middleware.py +0 -0
  158. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/responses.py +0 -0
  159. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/api/routers.py +0 -0
  160. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/config/__init__.py +0 -0
  161. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/config/sources.py +0 -0
  162. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/decorators.py +0 -0
  163. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/redis/__init__.py +0 -0
  164. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/registry.py +0 -0
  165. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/base.py +0 -0
  166. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/registry.py +0 -0
  167. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/table.py +0 -0
  168. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/__init__.py +0 -0
  169. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/connections/sql/query.py +0 -0
  170. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/crypto/enum.py +0 -0
  171. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/__init__.py +0 -0
  172. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/expressions/target.py +0 -0
  173. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/data/record.py +0 -0
  174. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/decorators/__init__.py +0 -0
  175. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/decorators/warnings.py +0 -0
  176. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/docker/__init__.py +0 -0
  177. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/extract/__init__.py +0 -0
  178. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/extract/schemas.py +0 -0
  179. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/load/__init__.py +0 -0
  180. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/load/loader.py +0 -0
  181. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/load/schemas.py +0 -0
  182. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/transform/__init__.py +0 -0
  183. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/etl/transform/schemas.py +0 -0
  184. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/__init__.py +0 -0
  185. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/enum.py +0 -0
  186. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/frames.py +0 -0
  187. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/events/models.py +0 -0
  188. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/exceptions/custom/__init__.py +0 -0
  189. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/exceptions/http/__init__.py +0 -0
  190. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/files/toml.py +0 -0
  191. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/http/exponential_backoff.py +0 -0
  192. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/jobs/__init__.py +0 -0
  193. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/jobs/registry.py +0 -0
  194. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/log_monitor/__init__.py +0 -0
  195. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/log_monitor/constants.py +0 -0
  196. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/notifications/__init__.py +0 -0
  197. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/notifications/models.py +0 -0
  198. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/observability/timing/__init__.py +0 -0
  199. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/observability/timing/constants.py +0 -0
  200. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/py.typed +0 -0
  201. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/registry/ordered.py +0 -0
  202. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/dataclasses/__init__.py +0 -0
  203. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/models/__init__.py +0 -0
  204. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/models/arbitrary.py +0 -0
  205. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/models/date_models.py +0 -0
  206. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/pydantic/__init__.py +0 -0
  207. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/schemas/pydantic/fields.py +0 -0
  208. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/serialization/enum.py +0 -0
  209. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/coercion.py +0 -0
  210. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/ids.py +0 -0
  211. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/raise_exc.py +0 -0
  212. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/text.py +0 -0
  213. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/time.py +0 -0
  214. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/validators.py +0 -0
  215. {python_corekit-0.2.0 → python_corekit-0.4.0}/corekit/utils/void.py +0 -0
  216. {python_corekit-0.2.0 → python_corekit-0.4.0}/python_corekit.egg-info/dependency_links.txt +0 -0
  217. {python_corekit-0.2.0 → python_corekit-0.4.0}/python_corekit.egg-info/top_level.txt +0 -0
  218. {python_corekit-0.2.0 → python_corekit-0.4.0}/setup.cfg +0 -0
  219. {python_corekit-0.2.0 → python_corekit-0.4.0}/setup.py +0 -0
  220. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_api.py +0 -0
  221. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_concurrency.py +0 -0
  222. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_imports.py +0 -0
  223. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_imports_are_top_level.py +0 -0
  224. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_middleware_stack.py +0 -0
  225. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_ordered_registry.py +0 -0
  226. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_utils_coercion.py +0 -0
  227. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_utils_ids.py +0 -0
  228. {python_corekit-0.2.0 → python_corekit-0.4.0}/tests/test_utils_text.py +0 -0
  229. {python_corekit-0.2.0 → python_corekit-0.4.0}/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.2.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.0
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.2.0
60
+ python-corekit~=0.4.0
59
61
  ```
60
62
 
61
63
  Before 1.0, the minor version carries breaking changes.
@@ -281,10 +283,36 @@ ceiling, not 9,999 threads.
281
283
  Failures propagate by default. Pass `raise_on_error=False` to log and skip them
282
284
  instead, which loses results silently and so is opt-in.
283
285
 
286
+ ## LLM chat and tools
287
+
288
+ OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
289
+ ``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
290
+ server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
291
+ facade.
292
+
293
+ ```python
294
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
295
+
296
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
297
+ prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
298
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
299
+
300
+ registry = ToolRegistry()
301
+ registry.register(MyTool())
302
+ loop = ToolLoop(client, registry)
303
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
304
+ ...
305
+ ```
306
+
307
+ ``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
308
+ (``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
309
+ *text* stays with the consumer — corekit only ships the loader. See
310
+ `docs/LLM_EXTRACTION.md`.
311
+
284
312
  ## HTTP clients
285
313
 
286
314
  ```python
287
- from corekit.http.client import BaseApiClient
315
+ from corekit.http import BaseApiClient
288
316
 
289
317
  class GithubClient(BaseApiClient):
290
318
  """
@@ -301,7 +329,8 @@ response.data["login"]
301
329
 
302
330
  Retries 429 and 5xx with exponential backoff. Every response is a
303
331
  `BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
304
- raising. `async_get`, `async_post` and friends do the same without blocking.
332
+ raising. The same methods are awaitable inside a running event loop:
333
+ `await client.get(...)`.
305
334
 
306
335
  ## Serialization
307
336
 
@@ -351,7 +380,7 @@ Settings are grouped by concern, so `get_settings().concurrency.max_threads`
351
380
  says where a value belongs. Environment variables use a double underscore for
352
381
  the section: `COREKIT_CONCURRENCY__MAX_THREADS=16`.
353
382
 
354
- Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO_SALT`). Empty
383
+ Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO__SALT`). Empty
355
384
  values are treated as unset, because container runtimes routinely pass `FOO=`
356
385
  for a variable that was never set.
357
386
 
@@ -375,7 +404,6 @@ Imports go downward only.
375
404
  ```
376
405
  corekit/
377
406
  config/ settings, sources, loader
378
- constants.py
379
407
 
380
408
  exceptions/ error types
381
409
 
@@ -390,10 +418,11 @@ corekit/
390
418
  connections/ the Connectable lifecycle and @connect
391
419
  sql/ SQLConnection, statements, migrations
392
420
  redis/ RedisConnection
393
- http/ BaseApiClient, retries, responses
421
+ http/ BaseHttpClient, BaseApiClient, retries, responses
394
422
 
395
423
  api/ Application, lifespan, middleware, routers
396
424
  docker/ notifications/ etl/
425
+ llm/ messages, tools, agentic loop (no GenerationClient)
397
426
 
398
427
  events/ log_monitor/ built on the capabilities above
399
428
  ```
@@ -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.2.0
24
+ python-corekit~=0.4.0
24
25
  ```
25
26
 
26
27
  Before 1.0, the minor version carries breaking changes.
@@ -246,10 +247,36 @@ ceiling, not 9,999 threads.
246
247
  Failures propagate by default. Pass `raise_on_error=False` to log and skip them
247
248
  instead, which loses results silently and so is opt-in.
248
249
 
250
+ ## LLM chat and tools
251
+
252
+ OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
253
+ ``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
254
+ server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
255
+ facade.
256
+
257
+ ```python
258
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
259
+
260
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
261
+ prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
262
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
263
+
264
+ registry = ToolRegistry()
265
+ registry.register(MyTool())
266
+ loop = ToolLoop(client, registry)
267
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
268
+ ...
269
+ ```
270
+
271
+ ``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
272
+ (``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
273
+ *text* stays with the consumer — corekit only ships the loader. See
274
+ `docs/LLM_EXTRACTION.md`.
275
+
249
276
  ## HTTP clients
250
277
 
251
278
  ```python
252
- from corekit.http.client import BaseApiClient
279
+ from corekit.http import BaseApiClient
253
280
 
254
281
  class GithubClient(BaseApiClient):
255
282
  """
@@ -266,7 +293,8 @@ response.data["login"]
266
293
 
267
294
  Retries 429 and 5xx with exponential backoff. Every response is a
268
295
  `BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
269
- raising. `async_get`, `async_post` and friends do the same without blocking.
296
+ raising. The same methods are awaitable inside a running event loop:
297
+ `await client.get(...)`.
270
298
 
271
299
  ## Serialization
272
300
 
@@ -316,7 +344,7 @@ Settings are grouped by concern, so `get_settings().concurrency.max_threads`
316
344
  says where a value belongs. Environment variables use a double underscore for
317
345
  the section: `COREKIT_CONCURRENCY__MAX_THREADS=16`.
318
346
 
319
- Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO_SALT`). Empty
347
+ Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO__SALT`). Empty
320
348
  values are treated as unset, because container runtimes routinely pass `FOO=`
321
349
  for a variable that was never set.
322
350
 
@@ -340,7 +368,6 @@ Imports go downward only.
340
368
  ```
341
369
  corekit/
342
370
  config/ settings, sources, loader
343
- constants.py
344
371
 
345
372
  exceptions/ error types
346
373
 
@@ -355,10 +382,11 @@ corekit/
355
382
  connections/ the Connectable lifecycle and @connect
356
383
  sql/ SQLConnection, statements, migrations
357
384
  redis/ RedisConnection
358
- http/ BaseApiClient, retries, responses
385
+ http/ BaseHttpClient, BaseApiClient, retries, responses
359
386
 
360
387
  api/ Application, lifespan, middleware, routers
361
388
  docker/ notifications/ etl/
389
+ llm/ messages, tools, agentic loop (no GenerationClient)
362
390
 
363
391
  events/ log_monitor/ built on the capabilities above
364
392
  ```
@@ -26,16 +26,34 @@ from typing import Any, Sequence
26
26
 
27
27
  from fastapi import FastAPI
28
28
  from fastapi.middleware.cors import CORSMiddleware
29
+ from fastapi.responses import JSONResponse
29
30
  from fastapi.staticfiles import StaticFiles
30
31
  from starlette.middleware.trustedhost import TrustedHostMiddleware
31
32
 
32
33
  from corekit.api.middleware import MiddlewareStack
33
34
  from corekit.api.routers import CatchAllRouter, SimpleRouter, router_registry
35
+ from corekit.exceptions import CoreHTTPException
34
36
  from corekit.observability.loggable import Loggable
35
37
 
36
38
  __all__ = ["Application"]
37
39
 
38
40
 
41
+ def _origin_in_package(router: SimpleRouter, package_name: str) -> bool:
42
+ """
43
+ Whether this router was constructed by ``package_name`` or a submodule.
44
+ """
45
+ origin = router._origin_module
46
+ return origin == package_name or origin.startswith(f"{package_name}.")
47
+
48
+
49
+ async def _core_http_exception_handler(request: Any, exc: CoreHTTPException) -> JSONResponse:
50
+ """
51
+ The same body FastAPI returns for ``HTTPException``.
52
+ """
53
+ headers = getattr(exc, "headers", None)
54
+ return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}, headers=headers)
55
+
56
+
39
57
  class Application(FastAPI, Loggable):
40
58
  """
41
59
  ``FastAPI`` plus router discovery, middleware helpers and static mounting.
@@ -47,6 +65,7 @@ class Application(FastAPI, Loggable):
47
65
  """
48
66
  FastAPI.__init__(self, *args, **kwargs)
49
67
  Loggable.__init__(self)
68
+ self._register_http_exception_handler()
50
69
 
51
70
  # ─── Routers ──────────────────────────────────────────────────────────
52
71
 
@@ -55,9 +74,12 @@ class Application(FastAPI, Loggable):
55
74
  Import every module under ``package`` and mount the routers it defines.
56
75
 
57
76
  Importing a module is what constructs its routers, so discovery is a
58
- walk over ``package`` that lets each one register itself. A router that
59
- was included into another router during that walk is skipped -- its
60
- parent already owns it -- and any ``CatchAllRouter`` is mounted last.
77
+ walk over ``package`` that lets each one register itself. Only routers
78
+ constructed inside that package are mounted: the registry is
79
+ process-wide, and an unclaimed router from some other import must not
80
+ ride along. A router that was included into another router is skipped
81
+ -- its parent already owns it -- and any ``CatchAllRouter`` is mounted
82
+ last.
61
83
 
62
84
  Args:
63
85
  package: The imported package to search, e.g. ``app.backend.routers``.
@@ -78,11 +100,12 @@ class Application(FastAPI, Loggable):
78
100
 
79
101
  self._warn_on_unimportable_dirs(package)
80
102
 
81
- before = len(router_registry)
82
103
  scanned = self._import_submodules(package)
83
- discovered = len(router_registry) - before
84
-
85
- mountable = router_registry.unclaimed
104
+ # unclaimed is every top-level router in the process. Mounting that
105
+ # list would include routers built by some other package that happens
106
+ # to be imported. Origin is where the instance was constructed, which
107
+ # is the scanned package for a router that belongs here.
108
+ mountable = [router for router in router_registry.unclaimed if _origin_in_package(router, name)]
86
109
  if not mountable:
87
110
  raise ValueError(
88
111
  f"No routers found in '{name}' (scanned {scanned} modules). "
@@ -90,8 +113,9 @@ class Application(FastAPI, Loggable):
90
113
  f"a SimpleRouter or SmartRouter at import time."
91
114
  )
92
115
 
93
- claimed = discovered - len(mountable)
94
- self.info(f" scanned {scanned} modules, found {discovered} routers")
116
+ in_package = [router for router in router_registry if _origin_in_package(router, name)]
117
+ claimed = len(in_package) - len(mountable)
118
+ self.info(f" scanned {scanned} modules, found {len(in_package)} routers")
95
119
  if claimed > 0:
96
120
  plural = "s" if claimed != 1 else ""
97
121
  self.info(f" skipped {claimed} sub-router{plural} (already included by a parent)")
@@ -235,3 +259,17 @@ class Application(FastAPI, Loggable):
235
259
  self.mount(url_path, StaticFiles(directory=str(path), **kwargs), name=name or url_path.strip("/"))
236
260
  self.info(f" + static {url_path:<24} -> {path}")
237
261
  return True
262
+
263
+ def _register_http_exception_handler(self) -> None:
264
+ """
265
+ Handle the library's HTTP exceptions the same way FastAPI handles its own.
266
+
267
+ ``CoreHTTPException`` is already an ``HTTPException``, so Starlette
268
+ would answer it. Registering the type explicitly keeps that response
269
+ -- ``{"detail": ...}`` only, never ``message`` or ``error`` -- even
270
+ if a later change in lookup order would otherwise miss the subclass.
271
+ A handler the caller already installed is left in place.
272
+ """
273
+ if CoreHTTPException in self.exception_handlers:
274
+ return
275
+ self.add_exception_handler(CoreHTTPException, _core_http_exception_handler)
@@ -108,7 +108,9 @@ class Lifespan(Loggable):
108
108
  startup: Called on the way up, in the order steps were added.
109
109
  shutdown: Called on the way down, in reverse order. Called only if
110
110
  ``startup`` completed, so it can assume what ``startup`` builds.
111
- tags: Free-form labels, for callers that group or filter steps.
111
+ tags: Labels used by ``for_tags``. Untagged steps always run; a
112
+ tagged step runs only when one of its tags is selected. With
113
+ no filter, every step runs.
112
114
  always_shutdown: Call ``shutdown`` even when ``startup`` raised.
113
115
  For a startup that acquires something before it can fail -- a
114
116
  connection opened, then a handshake that throws. The hook then
@@ -136,6 +138,25 @@ class Lifespan(Loggable):
136
138
  """The registered steps, in the order they will start."""
137
139
  return list(self._steps)
138
140
 
141
+ def for_tags(self, *tags: str) -> "Lifespan":
142
+ """
143
+ A lifespan that runs the steps matching ``tags``, plus untagged ones.
144
+
145
+ An untagged step is ordinary startup and always runs. A tagged step
146
+ runs only when the caller asked for one of its tags, so a label is a
147
+ filter rather than decoration. No tags means every step, which is
148
+ what ``__call__`` does.
149
+ """
150
+ if not tags:
151
+ return self
152
+
153
+ wanted = set(tags)
154
+ selected = Lifespan()
155
+ for step in self._steps:
156
+ if not step.tags or wanted.intersection(step.tags):
157
+ selected._steps.append(step)
158
+ return selected
159
+
139
160
  @asynccontextmanager
140
161
  async def __call__(self, app: Any = None) -> AsyncIterator[None]:
141
162
  """
@@ -166,7 +187,8 @@ class Lifespan(Loggable):
166
187
  stack.push_async_callback(self._run_shutdown, step)
167
188
 
168
189
  if step.startup is not None:
169
- self.info(f" ^ {step.name}")
190
+ label = step.name if not step.tags else f"{step.name} [{', '.join(step.tags)}]"
191
+ self.info(f" ^ {label}")
170
192
  try:
171
193
  await self._maybe_await(step.startup)
172
194
  except Exception:
@@ -192,7 +214,8 @@ class Lifespan(Loggable):
192
214
  self.warning(f" - {step.name} (skipped: startup did not complete)")
193
215
  return
194
216
 
195
- self.info(f" v {step.name}")
217
+ label = step.name if not step.tags else f"{step.name} [{', '.join(step.tags)}]"
218
+ self.info(f" v {label}")
196
219
  try:
197
220
  await self._maybe_await(step.shutdown)
198
221
  except Exception:
@@ -2,8 +2,8 @@
2
2
  Concurrency primitives: per-thread storage, workers, and parallel mapping.
3
3
  """
4
4
 
5
- from corekit.concurrency.decorators import parallelize
5
+ from corekit.concurrency.decorators import allow_sync, parallelize
6
6
  from corekit.concurrency.thread_local import ThreadLocalRegistry
7
7
  from corekit.concurrency.worker import ThreadWorker
8
8
 
9
- __all__ = ["ThreadLocalRegistry", "ThreadWorker", "parallelize"]
9
+ __all__ = ["ThreadLocalRegistry", "ThreadWorker", "allow_sync", "parallelize"]
@@ -18,19 +18,26 @@ machine rather than an error message.
18
18
 
19
19
  Results arrive out of order, because they arrive as they complete. Pass
20
20
  ``ordered=True`` to get them in the order the inputs were given.
21
+
22
+ ``allow_sync`` wraps an async function so a script can call it directly, while
23
+ an already-running event loop still gets a coroutine to await.
21
24
  """
22
25
 
26
+ import asyncio
23
27
  import functools
28
+ import logging
29
+ from collections.abc import Awaitable, Callable
24
30
  from concurrent.futures import Future, ThreadPoolExecutor, as_completed
25
- from typing import Any, Callable, Iterable, Iterator
31
+ from typing import Any, Iterable, Iterator, ParamSpec, TypeVar
26
32
 
27
33
  from corekit.config import get_settings
28
- from corekit.observability import Loggable
29
34
 
30
- __all__ = ["parallelize"]
35
+ __all__ = ["parallelize", "allow_sync"]
36
+
37
+ _logger = logging.getLogger("parallelize")
31
38
 
32
- _logger = Loggable()
33
- _logger.logger = _logger.logger.getChild("parallelize")
39
+ P = ParamSpec("P")
40
+ R = TypeVar("R")
34
41
 
35
42
 
36
43
  def parallelize(
@@ -70,3 +77,23 @@ def parallelize(
70
77
  return wrapper
71
78
 
72
79
  return decorator
80
+
81
+
82
+ def allow_sync(async_func: Callable[P, Awaitable[R]]) -> Callable[P, R | Awaitable[R]]:
83
+ """
84
+ Call an async function from sync code, or return its coroutine if a loop is running.
85
+
86
+ A script gets the result back immediately. Inside an async app the wrapper
87
+ returns the coroutine, so the caller must await it — calling the wrapper
88
+ and ignoring the result will not run the function.
89
+ """
90
+
91
+ @functools.wraps(async_func)
92
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Awaitable[R]:
93
+ try:
94
+ asyncio.get_running_loop()
95
+ except RuntimeError:
96
+ return asyncio.run(async_func(*args, **kwargs))
97
+ return async_func(*args, **kwargs)
98
+
99
+ return wrapper
@@ -19,7 +19,7 @@ import threading
19
19
  from typing import Any
20
20
 
21
21
  from corekit.observability.loggable import Loggable
22
- from corekit.registry import SmartRegistry
22
+ from corekit.registry import normalize_key
23
23
 
24
24
  __all__ = ["ThreadLocalRegistry"]
25
25
 
@@ -59,7 +59,7 @@ class ThreadLocalRegistry(threading.local, Loggable):
59
59
  turn SQLConnection into "sqlconnection" rather than "sql_connection".
60
60
  """
61
61
  name = key.__name__ if isinstance(key, type) else str(key)
62
- return SmartRegistry.__normalize_key__(name).replace("-", "_")
62
+ return normalize_key(name).replace("-", "_")
63
63
 
64
64
  def get(self, key: Any, fallback: Any = None) -> Any:
65
65
  """
@@ -5,6 +5,15 @@ import time
5
5
 
6
6
 
7
7
  class ThreadWorker(threading.Thread):
8
+ """
9
+ A daemon thread that waits for ``can_start``, then calls ``process`` once.
10
+
11
+ ``timeout`` and ``loop`` are stored for a subclass that wants them.
12
+ ``run`` does not enforce ``timeout`` and does not drive ``loop``.
13
+ ``stop`` sets ``running`` so a subclass of ``process`` can notice it; it
14
+ does not interrupt a thread already inside ``process``.
15
+ """
16
+
8
17
  def __init__(
9
18
  self,
10
19
  can_start: threading.Event,
@@ -9,6 +9,8 @@ settings know only their own shape.
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
 
12
+ from pydantic import ValidationError
13
+
12
14
  from corekit.config.settings import ENV_PREFIX, CorekitSettings
13
15
  from corekit.config.sources import ConfigFileSource, ConfigSource, EnvironmentSource, PyprojectSource
14
16
  from corekit.observability import Loggable
@@ -72,11 +74,12 @@ class SettingsLoader(Loggable):
72
74
  """
73
75
  Read every source and construct the settings.
74
76
 
75
- Invalid configuration falls back to defaults rather than raising:
76
- corekit has to remain importable, and a library that refuses to load
77
- because of a stray value in a file is worse than one that logs the
78
- problem and carries on. The log line is the difference between this and
79
- silently ignoring it.
77
+ Invalid configuration drops the bad section and keeps the rest.
78
+ A bad thread count must not discard a valid salt or serialization
79
+ key that arrived from another section or the environment. If what
80
+ remains still will not validate, the last resort is defaults --
81
+ corekit has to remain importable. The log line is the difference
82
+ between this and silently ignoring it.
80
83
  """
81
84
  values: dict[str, Any] = {}
82
85
  for source in self.sources():
@@ -84,10 +87,44 @@ class SettingsLoader(Loggable):
84
87
 
85
88
  try:
86
89
  return CorekitSettings(**values)
90
+ except ValidationError as exc:
91
+ cleaned, dropped = self._without_invalid_sections(values, exc)
92
+ if dropped:
93
+ self.error(
94
+ f"Invalid corekit configuration in {', '.join(dropped)}; "
95
+ f"those sections fall back to defaults: {exc}"
96
+ )
97
+ try:
98
+ return CorekitSettings(**cleaned)
99
+ except ValidationError as retry_exc:
100
+ self.error(f"Invalid corekit configuration, falling back to defaults: {retry_exc}")
101
+ return CorekitSettings()
102
+ self.error(f"Invalid corekit configuration, falling back to defaults: {exc}")
103
+ return CorekitSettings()
87
104
  except Exception as exc:
88
105
  self.error(f"Invalid corekit configuration, falling back to defaults: {exc}")
89
106
  return CorekitSettings()
90
107
 
108
+ @staticmethod
109
+ def _without_invalid_sections(values: dict[str, Any], exc: ValidationError) -> tuple[dict[str, Any], list[str]]:
110
+ """
111
+ Drop each section named in a validation error, leaving the others.
112
+
113
+ A section is the first element of the error location. A failure that
114
+ names no section cannot be isolated, so nothing is dropped and the
115
+ caller falls back to defaults.
116
+ """
117
+ dropped: list[str] = []
118
+ for error in exc.errors():
119
+ loc = error.get("loc") or ()
120
+ if not loc:
121
+ continue
122
+ section = str(loc[0])
123
+ if section not in dropped:
124
+ dropped.append(section)
125
+ cleaned = {key: value for key, value in values.items() if key not in dropped}
126
+ return cleaned, dropped
127
+
91
128
  @staticmethod
92
129
  def _merge(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
93
130
  """
@@ -35,6 +35,10 @@ class StandardsSettings(BaseModel):
35
35
 
36
36
  Off by default. These are conventions, not library invariants, and crashing
37
37
  a consumer's application over a missing docstring would be hostile.
38
+
39
+ ``strict_mode`` makes a ``SmartRegistry`` refuse a second write to a
40
+ normalized key instead of warning and replacing. It does not imply
41
+ ``require_handler_docstrings``; that check stays its own flag.
38
42
  """
39
43
 
40
44
  require_handler_docstrings: bool = False
@@ -80,7 +84,13 @@ class ConcurrencySettings(BaseModel):
80
84
  @staticmethod
81
85
  def cpu_default() -> int:
82
86
  """
83
- A reasonable thread count for this machine, for callers that want one.
87
+ A machine-sized thread count, for a caller that asks for one.
88
+
89
+ Not what ``resolve`` or ``parallelize`` use when the caller is silent.
90
+ Those use ``default_threads``. Pass this in when the work should
91
+ scale with the machine::
92
+
93
+ parallelize(num_threads=ConcurrencySettings.cpu_default())
84
94
  """
85
95
  return min(32, (os.cpu_count() or 1) * 5)
86
96
 
@@ -17,7 +17,12 @@ implementations of the abstraction directly above them, and there is no useful
17
17
  way to think about one without the other.
18
18
  """
19
19
 
20
- from corekit.connections.connectable import Connectable, ConnectableType, ConnectionPreference
20
+ from corekit.connections.connectable import (
21
+ Connectable,
22
+ ConnectableType,
23
+ ConnectionPreference,
24
+ ReadOnlyConnectionError,
25
+ )
21
26
  from corekit.connections.decorators import connect
22
27
  from corekit.connections.registry import ConnectionRegistry, registry
23
28
 
@@ -26,6 +31,7 @@ __all__ = [
26
31
  "ConnectableType",
27
32
  "ConnectionPreference",
28
33
  "ConnectionRegistry",
34
+ "ReadOnlyConnectionError",
29
35
  "connect",
30
36
  "registry",
31
37
  ]