python-corekit 0.1.0__tar.gz → 0.2.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 (198) hide show
  1. {python_corekit-0.1.0/python_corekit.egg-info → python_corekit-0.2.0}/PKG-INFO +105 -100
  2. {python_corekit-0.1.0 → python_corekit-0.2.0}/README.md +102 -97
  3. python_corekit-0.2.0/corekit/api/__init__.py +24 -0
  4. python_corekit-0.2.0/corekit/api/application.py +237 -0
  5. python_corekit-0.2.0/corekit/api/lifespan.py +210 -0
  6. python_corekit-0.2.0/corekit/api/middleware.py +93 -0
  7. python_corekit-0.2.0/corekit/api/routers.py +223 -0
  8. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/concurrency/worker.py +65 -65
  9. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/config/settings.py +3 -3
  10. python_corekit-0.2.0/corekit/connections/sql/__init__.py +38 -0
  11. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/connection.py +19 -0
  12. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/migration/__init__.py +5 -5
  13. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/migration/base.py +3 -3
  14. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/migration/operations.py +66 -42
  15. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/migration/registry.py +2 -2
  16. python_corekit-0.2.0/corekit/connections/sql/operations/__init__.py +24 -0
  17. python_corekit-0.2.0/corekit/connections/sql/operations/base.py +102 -0
  18. python_corekit-0.2.0/corekit/connections/sql/operations/statements.py +150 -0
  19. python_corekit-0.2.0/corekit/connections/sql/query.py +10 -0
  20. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/table.py +30 -4
  21. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/constants.py +45 -45
  22. python_corekit-0.2.0/corekit/crypto/constants.py +7 -0
  23. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/data/__init__.py +8 -0
  24. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/data/expressions/__init__.py +10 -2
  25. python_corekit-0.2.0/corekit/data/expressions/comparison.py +332 -0
  26. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/data/expressions/expression.py +103 -98
  27. python_corekit-0.2.0/corekit/data/expressions/operator.py +54 -0
  28. python_corekit-0.2.0/corekit/data/expressions/target.py +21 -0
  29. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/data/record.py +147 -147
  30. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/data/stats.py +159 -157
  31. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/decorators/__init__.py +2 -2
  32. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/decorators/exception_handling.py +2 -1
  33. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/connection.py +44 -44
  34. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/websocket.py +3 -2
  35. python_corekit-0.2.0/corekit/exceptions/__init__.py +18 -0
  36. python_corekit-0.2.0/corekit/http/__init__.py +13 -0
  37. python_corekit-0.2.0/corekit/jobs/__init__.py +26 -0
  38. python_corekit-0.2.0/corekit/jobs/registry.py +87 -0
  39. python_corekit-0.2.0/corekit/jobs/runner.py +69 -0
  40. python_corekit-0.2.0/corekit/jobs/task.py +152 -0
  41. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/observability/__init__.py +5 -3
  42. python_corekit-0.2.0/corekit/observability/request_context.py +135 -0
  43. python_corekit-0.2.0/corekit/registry/__init__.py +17 -0
  44. python_corekit-0.2.0/corekit/registry/ordered.py +86 -0
  45. python_corekit-0.2.0/corekit/schemas/__init__.py +10 -0
  46. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/schemas/enum.py +49 -49
  47. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/schemas/models/arbitrary.py +11 -11
  48. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/schemas/pydantic/fields.py +35 -35
  49. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/schemas/types.py +40 -40
  50. python_corekit-0.2.0/corekit/serialization/__init__.py +22 -0
  51. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/serialization/serializer.py +1 -1
  52. python_corekit-0.2.0/corekit/utils/__init__.py +59 -0
  53. python_corekit-0.2.0/corekit/utils/coercion.py +118 -0
  54. python_corekit-0.2.0/corekit/utils/collections.py +115 -0
  55. python_corekit-0.2.0/corekit/utils/ids.py +61 -0
  56. python_corekit-0.2.0/corekit/utils/payload.py +100 -0
  57. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/utils/raise_exc.py +8 -8
  58. python_corekit-0.2.0/corekit/utils/text.py +56 -0
  59. python_corekit-0.2.0/corekit/utils/time.py +74 -0
  60. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/utils/validators.py +15 -15
  61. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/utils/void.py +8 -8
  62. {python_corekit-0.1.0 → python_corekit-0.2.0}/pyproject.toml +3 -3
  63. {python_corekit-0.1.0 → python_corekit-0.2.0/python_corekit.egg-info}/PKG-INFO +105 -100
  64. {python_corekit-0.1.0 → python_corekit-0.2.0}/python_corekit.egg-info/SOURCES.txt +34 -1
  65. python_corekit-0.2.0/tests/test_application.py +315 -0
  66. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_architecture.py +1 -0
  67. python_corekit-0.2.0/tests/test_expressions.py +255 -0
  68. python_corekit-0.2.0/tests/test_expressions_sqlalchemy.py +198 -0
  69. python_corekit-0.2.0/tests/test_jobs.py +167 -0
  70. python_corekit-0.2.0/tests/test_lifespan.py +225 -0
  71. python_corekit-0.2.0/tests/test_middleware_stack.py +134 -0
  72. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_migration.py +59 -10
  73. python_corekit-0.2.0/tests/test_operations.py +159 -0
  74. python_corekit-0.2.0/tests/test_ordered_registry.py +78 -0
  75. python_corekit-0.2.0/tests/test_request_context.py +131 -0
  76. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_sql.py +58 -0
  77. python_corekit-0.2.0/tests/test_utils_coercion.py +94 -0
  78. python_corekit-0.2.0/tests/test_utils_collections.py +84 -0
  79. python_corekit-0.2.0/tests/test_utils_ids.py +51 -0
  80. python_corekit-0.2.0/tests/test_utils_payload.py +65 -0
  81. python_corekit-0.2.0/tests/test_utils_text.py +56 -0
  82. python_corekit-0.2.0/tests/test_utils_time.py +62 -0
  83. python_corekit-0.1.0/corekit/api/__init__.py +0 -9
  84. python_corekit-0.1.0/corekit/api/routers.py +0 -115
  85. python_corekit-0.1.0/corekit/connections/sql/__init__.py +0 -10
  86. python_corekit-0.1.0/corekit/connections/sql/query.py +0 -68
  87. python_corekit-0.1.0/corekit/crypto/constants.py +0 -7
  88. python_corekit-0.1.0/corekit/data/expressions/comparison.py +0 -252
  89. python_corekit-0.1.0/corekit/registry/__init__.py +0 -12
  90. python_corekit-0.1.0/corekit/schemas/dataclasses/__init__.py +0 -0
  91. python_corekit-0.1.0/corekit/schemas/models/__init__.py +0 -0
  92. python_corekit-0.1.0/corekit/schemas/pydantic/__init__.py +0 -0
  93. python_corekit-0.1.0/corekit/serialization/__init__.py +0 -0
  94. python_corekit-0.1.0/corekit/utils/__init__.py +0 -5
  95. python_corekit-0.1.0/corekit/utils/ids.py +0 -5
  96. python_corekit-0.1.0/corekit/utils/time.py +0 -21
  97. {python_corekit-0.1.0 → python_corekit-0.2.0}/LICENSE +0 -0
  98. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/__init__.py +0 -0
  99. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/api/handler.py +0 -0
  100. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/api/responses.py +0 -0
  101. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/concurrency/__init__.py +0 -0
  102. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/concurrency/decorators.py +0 -0
  103. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/concurrency/thread_local.py +0 -0
  104. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/config/__init__.py +0 -0
  105. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/config/loader.py +0 -0
  106. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/config/sources.py +0 -0
  107. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/__init__.py +0 -0
  108. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/connectable.py +0 -0
  109. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/decorators.py +0 -0
  110. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/redis/__init__.py +0 -0
  111. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/redis/connection.py +0 -0
  112. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/registry.py +0 -0
  113. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/fields/__init__.py +0 -0
  114. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/fields/jsonb.py +0 -0
  115. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/connections/sql/migration/table.py +0 -0
  116. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/crypto/__init__.py +0 -0
  117. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/crypto/enum.py +0 -0
  118. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/crypto/hasher.py +0 -0
  119. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/data/dataset.py +0 -0
  120. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/decorators/warnings.py +0 -0
  121. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/docker/__init__.py +0 -0
  122. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/docker/watchdog.py +0 -0
  123. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/__init__.py +0 -0
  124. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/extract/__init__.py +0 -0
  125. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/extract/extractor.py +0 -0
  126. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/extract/schemas.py +0 -0
  127. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/load/__init__.py +0 -0
  128. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/load/loader.py +0 -0
  129. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/load/schemas.py +0 -0
  130. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/orchestrator.py +0 -0
  131. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/schemas.py +0 -0
  132. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/transform/__init__.py +0 -0
  133. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/transform/schemas.py +0 -0
  134. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/etl/transform/transformer.py +0 -0
  135. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/__init__.py +0 -0
  136. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/enum.py +0 -0
  137. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/frames.py +0 -0
  138. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/models.py +0 -0
  139. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/publisher.py +0 -0
  140. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/reader.py +0 -0
  141. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/events/sse.py +0 -0
  142. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/exceptions/base.py +0 -0
  143. {python_corekit-0.1.0/corekit/exceptions → python_corekit-0.2.0/corekit/exceptions/custom}/__init__.py +0 -0
  144. {python_corekit-0.1.0/corekit/exceptions/custom → python_corekit-0.2.0/corekit/exceptions/http}/__init__.py +0 -0
  145. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/exceptions/http/exceptions.py +0 -0
  146. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/exceptions/types.py +0 -0
  147. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/files/__init__.py +0 -0
  148. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/files/base.py +0 -0
  149. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/files/enum.py +0 -0
  150. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/files/json.py +0 -0
  151. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/files/pickle.py +0 -0
  152. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/files/toml.py +0 -0
  153. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/http/client.py +0 -0
  154. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/http/exponential_backoff.py +0 -0
  155. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/http/response.py +0 -0
  156. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/log_monitor/__init__.py +0 -0
  157. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/log_monitor/constants.py +0 -0
  158. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/log_monitor/models.py +0 -0
  159. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/log_monitor/service.py +0 -0
  160. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/notifications/__init__.py +0 -0
  161. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/notifications/base.py +0 -0
  162. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/notifications/models.py +0 -0
  163. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/observability/benchmarkable.py +0 -0
  164. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/observability/loggable.py +0 -0
  165. {python_corekit-0.1.0/corekit/exceptions/http → python_corekit-0.2.0/corekit/observability/timing}/__init__.py +0 -0
  166. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/observability/timing/constants.py +0 -0
  167. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/observability/timing/split.py +0 -0
  168. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/observability/timing/timer.py +0 -0
  169. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/py.typed +0 -0
  170. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/registry/registry.py +0 -0
  171. {python_corekit-0.1.0/corekit/http → python_corekit-0.2.0/corekit/schemas/dataclasses}/__init__.py +0 -0
  172. {python_corekit-0.1.0/corekit/observability/timing → python_corekit-0.2.0/corekit/schemas/models}/__init__.py +0 -0
  173. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/schemas/models/date_models.py +0 -0
  174. {python_corekit-0.1.0/corekit/schemas → python_corekit-0.2.0/corekit/schemas/pydantic}/__init__.py +0 -0
  175. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/serialization/enum.py +0 -0
  176. {python_corekit-0.1.0 → python_corekit-0.2.0}/corekit/serialization/serializable.py +0 -0
  177. {python_corekit-0.1.0 → python_corekit-0.2.0}/python_corekit.egg-info/dependency_links.txt +0 -0
  178. {python_corekit-0.1.0 → python_corekit-0.2.0}/python_corekit.egg-info/requires.txt +0 -0
  179. {python_corekit-0.1.0 → python_corekit-0.2.0}/python_corekit.egg-info/top_level.txt +0 -0
  180. {python_corekit-0.1.0 → python_corekit-0.2.0}/setup.cfg +0 -0
  181. {python_corekit-0.1.0 → python_corekit-0.2.0}/setup.py +0 -0
  182. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_api.py +0 -0
  183. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_concurrency.py +0 -0
  184. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_config.py +0 -0
  185. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_connections.py +0 -0
  186. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_data.py +0 -0
  187. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_docker.py +0 -0
  188. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_etl.py +0 -0
  189. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_events.py +0 -0
  190. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_files.py +0 -0
  191. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_imports.py +0 -0
  192. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_imports_are_top_level.py +0 -0
  193. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_log_monitor.py +0 -0
  194. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_redis.py +0 -0
  195. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_registry.py +0 -0
  196. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_requests.py +0 -0
  197. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_serialization.py +0 -0
  198. {python_corekit-0.1.0 → python_corekit-0.2.0}/tests/test_utils.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-corekit
3
- Version: 0.1.0
4
- Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI routers/handlers, data stores, and ETL
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
5
5
  Author: Steven Jacobsen
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/stevejaker/corekit
8
8
  Project-URL: Issues, https://github.com/stevejaker/corekit/issues
9
- Keywords: fastapi,etl,homelab,logging,benchmarking
9
+ Keywords: fastapi,etl,logging,benchmarking,sqlmodel,migrations
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3.11
@@ -36,8 +36,8 @@ Dynamic: license-file
36
36
  # corekit
37
37
 
38
38
  Shared foundations for Python projects: structured logging, benchmarking,
39
- registries, FastAPI routers with built-in handlers, an in-memory record store,
40
- and ETL scaffolding.
39
+ registries, a FastAPI application with routers and handlers, SQL statements and
40
+ migrations, background tasks, an in-memory record store, and ETL scaffolding.
41
41
 
42
42
  Requires Python 3.11+.
43
43
 
@@ -47,17 +47,15 @@ Requires Python 3.11+.
47
47
  pip install python-corekit
48
48
  ```
49
49
 
50
- No credentials, no SSH key, no token — which means a project that depends on
51
- corekit can be cloned and built by anyone, including inside a Docker build.
50
+ The distribution is `python-corekit`; the import is `corekit`.
52
51
 
53
52
  Every dependency corekit needs is installed with it. There are no optional
54
53
  extras to remember, and no import that fails because something was left out.
55
54
 
56
- The distribution is `python-corekit`; the import is `corekit`. Pin a compatible
57
- release rather than tracking whatever is newest:
55
+ Pin a compatible release rather than tracking whatever is newest:
58
56
 
59
57
  ```
60
- python-corekit~=0.1.0
58
+ python-corekit~=0.2.0
61
59
  ```
62
60
 
63
61
  Before 1.0, the minor version carries breaking changes.
@@ -90,6 +88,57 @@ class Report(Benchmarkable):
90
88
  self.timing("queried") # logs the time since the previous split
91
89
  ```
92
90
 
91
+ ## Assembling an application
92
+
93
+ `Application` is a plain `FastAPI` subclass — every constructor argument,
94
+ including `lifespan`, passes straight through. What it adds is a short set of
95
+ assembly steps, each of which logs what it did.
96
+
97
+ ```python
98
+ from corekit.api import Application, Lifespan
99
+ from app.backend import routers
100
+
101
+ lifespan = Lifespan()
102
+ lifespan.add("cache", startup=cache.connect, shutdown=cache.disconnect)
103
+
104
+ app = Application(lifespan=lifespan)
105
+ app.discover_routers(routers)
106
+ ```
107
+
108
+ `discover_routers` imports every module under the package so each router can
109
+ register itself. It raises if it finds none: passing a package is a statement
110
+ that routers live there, and an app that silently serves nothing is worse than
111
+ one that refuses to start.
112
+
113
+ Lifespan steps start in the order added and shut down in reverse, the way
114
+ nested `with` blocks unwind. If a step fails on the way up, the steps that
115
+ already started are still torn down. A shutdown hook runs only if its own
116
+ startup completed, so it may assume the state that startup builds — and a hook
117
+ that raises cannot stop the unwind.
118
+
119
+ ### Middleware
120
+
121
+ Middleware order is a security property: a host check that reads a client
122
+ address before the proxy-header layer has rewritten it is checking the proxy,
123
+ not the client. So middleware is never auto-discovered, and corekit installs
124
+ none by default. A stack is declared in one place, outermost first — the order
125
+ a request actually meets the layers.
126
+
127
+ ```python
128
+ from corekit.api import MiddlewareStack
129
+
130
+ stack = (
131
+ MiddlewareStack()
132
+ .add(ProxyHeadersMiddleware, trusted_hosts="*")
133
+ .add(TrustedHostMiddleware, allowed_hosts=HOSTS)
134
+ .add(CORSMiddleware, allow_origins=ORIGINS, allow_credentials=True)
135
+ )
136
+ app.add_middleware_stack(stack)
137
+ ```
138
+
139
+ Reading top to bottom gives the order a request travels, which is the property
140
+ you need when reviewing it.
141
+
93
142
  ## Routers and handlers
94
143
 
95
144
  A router and the handler holding its business logic travel together. Declare the
@@ -149,113 +198,67 @@ people.get_record("Ada").age # O(1) lookup by id
149
198
 
150
199
  Stores pickle cleanly, including their dynamically generated record class.
151
200
 
152
- ## Homelab pieces
201
+ ## SQL statements
153
202
 
154
- ### Container control
203
+ Select, insert, update and delete are objects you build and then execute, so a
204
+ statement can be assembled in pieces and passed around before it runs.
155
205
 
156
206
  ```python
157
- from corekit.docker import Watchdog
207
+ from corekit.connections.sql import SQLConnection, Select, Insert, Update, Delete
208
+ from corekit.data import Field
158
209
 
159
- watchdog = Watchdog(enforce_label=True)
160
- watchdog.restart_container_by_name("minecraft")
161
- watchdog.find_and_stop(label="app", value="staging")
162
- ```
210
+ conn = SQLConnection("sqlite:///app.db")
163
211
 
164
- `enforce_label` limits the blast radius: with it on, only containers carrying
165
- the `watchdog=true` label can be started, stopped or paused, so a mistyped name
166
- cannot take down something unrelated. Leave it on unless the watchdog is meant
167
- to control everything on the host.
168
-
169
- ### Reacting to logs
170
-
171
- Describe what to watch for and what to do about it:
172
-
173
- ```yaml
174
- # config.yaml
175
- containers:
176
- - name: "minecraft-.*"
177
- rules:
178
- - name: "out of memory"
179
- pattern: "java.lang.OutOfMemoryError"
180
- severity: critical
181
- send_notification: true
182
- actions:
183
- - type: restart_container
184
- max_restarts: 3
185
- restart_window: 3600
186
- advanced:
187
- ignore_patterns:
188
- - "healthcheck"
189
- rate_limits:
190
- restart_container:
191
- count: 5
192
- period: hour
193
- ```
212
+ conn.execute(Insert(table=User, rows=[{"name": "Ada", "age": 36}]).add(name="Bob", age=17))
194
213
 
195
- ```python
196
- from corekit.log_monitor import LogMonitor
214
+ adults = conn.fetch(Select(table=User).where(User.age >= 18).order_by(User.name).limit(10))
197
215
 
198
- LogMonitor.run("config.yaml")
216
+ conn.execute(Update(table=User).where(User.name == "Bob").set(age=18))
217
+ conn.execute(Delete(table=User).where(User.age < 13))
199
218
  ```
200
219
 
201
- Restarts are capped per container, so a crash loop cannot become a restart loop.
202
-
203
- ### Notifications
220
+ `where` accepts a SQLAlchemy expression or a `corekit.data` one, so the same
221
+ predicate language that filters a `Dataset` also filters a table:
204
222
 
205
223
  ```python
206
- from corekit.notifications import BaseNotificationService, Notification, NotificationType
207
-
208
- class DiscordNotifier(BaseNotificationService):
209
- """
210
- Sends notifications to a Discord channel.
211
- """
212
-
213
- def _send(self, message: str) -> None:
214
- discord.post(message)
215
-
216
- notifier.notify(Notification(message="disk full", type=NotificationType.ERROR))
224
+ Select(table=User).where(Field("age") >= 18)
217
225
  ```
218
226
 
219
- Override `_send`, not `send`. `notify()` formats the message and calls `_send`,
220
- so an override with any other name is silently ignored.
227
+ Update and delete compile to a single statement rather than fetching rows and
228
+ looping, which matters most over a network, where fetch-and-loop pays a round
229
+ trip per row.
221
230
 
222
- ### Real-time updates
231
+ A `Delete` or `Update` with no condition raises rather than running:
223
232
 
224
- Publish from wherever the work happens:
225
-
226
- ```python
227
- from corekit.events import EventPublisher
228
-
229
- publisher = EventPublisher.for_resource("minecraft", "server", "survival")
230
- publisher.publish("backup_finished", {"size": "4.2GB"})
233
+ ```
234
+ Delete on User needs a condition; use truncate to empty a table
231
235
  ```
232
236
 
233
- Stream it to the browser:
237
+ ## Background tasks
234
238
 
235
- ```python
236
- from corekit.api import SSEResponse
237
- from corekit.events import SSEStream
239
+ Tasks describe work and register themselves by name. Nothing here imports a
240
+ queue library, so the same task runs under RQ, Celery, a cron entry, or a test
241
+ with no queue at all.
238
242
 
239
- @router.get("/events")
240
- async def events(channel: str) -> SSEResponse:
241
- return SSEResponse(SSEStream(channel, keepalive_interval=15))
242
- ```
243
+ ```python
244
+ from corekit.jobs import Task, run_task
245
+ from corekit.utils import encode_payload
243
246
 
244
- The browser side is three lines, and reconnects on its own:
247
+ class SendDigest(Task):
248
+ def task_function(self, user: str) -> None:
249
+ ...
245
250
 
246
- ```javascript
247
- const source = new EventSource("/events?channel=minecraft:server:survival");
248
- source.addEventListener("backup_finished", e => console.log(JSON.parse(e.data)));
251
+ # a worker, holding only a name and a JSON string
252
+ run_task("SendDigest", encode_payload(["ada"]))
249
253
  ```
250
254
 
251
- `SSEStream` sends a `connected` frame on subscribe, an optional `initial_state`
252
- so a client arriving late renders immediately, and a comment frame every
253
- `keepalive_interval` seconds so proxies do not close an idle connection. For
254
- WebSockets, `WebSocketBridge` relays the same channel and stops on a terminal
255
- status.
255
+ Arguments cross the queue as JSON, never as a serialized object. `pickle` and
256
+ `dill` execute code while loading, so a queue holding objects turns write
257
+ access to the queue into code execution in a worker. `encode_payload` and
258
+ `decode_payload` live in `corekit.utils`, since crossing a process boundary as
259
+ data is not specific to queues.
256
260
 
257
- Publishing never raises: an event that cannot be delivered should not take down
258
- the operation that produced it. `publish` returns whether it worked.
261
+ `ScheduledTask` adds an `interval` for tasks a scheduler should repeat.
259
262
 
260
263
  ## Parallel work
261
264
 
@@ -371,23 +374,25 @@ Imports go downward only.
371
374
 
372
375
  ```
373
376
  corekit/
374
- config.py constants.py
377
+ config/ settings, sources, loader
378
+ constants.py
375
379
 
376
380
  exceptions/ error types
377
381
 
378
382
  observability/ Loggable, Benchmarkable, Timer
379
- registry/ schemas/ utils/ SmartRegistry, enums and fields, helpers
383
+ registry/ schemas/ utils/ registries, enums and fields, helpers
380
384
  data/ Dataset and its filter expressions
385
+ jobs/ queue-independent background tasks
381
386
  crypto/ files/ serialization/
382
387
  concurrency/ ThreadLocalRegistry, ThreadWorker
383
388
  decorators/
384
389
 
385
390
  connections/ the Connectable lifecycle and @connect
386
- sql/ SQLConnection, queries, migrations
391
+ sql/ SQLConnection, statements, migrations
387
392
  redis/ RedisConnection
388
393
  http/ BaseApiClient, retries, responses
389
394
 
390
- api/ handlers, routers, responses
395
+ api/ Application, lifespan, middleware, routers
391
396
  docker/ notifications/ etl/
392
397
 
393
398
  events/ log_monitor/ built on the capabilities above
@@ -404,13 +409,13 @@ layering deliberately.
404
409
  ## Development
405
410
 
406
411
  ```bash
407
- pip install -e ".[dev,all]"
412
+ pip install -e ".[dev]"
408
413
  pytest
409
414
  ruff format . && ruff check --fix .
410
415
  ```
411
416
 
412
- `tests/test_imports.py` imports every module in the package. Keep it passing:
413
- several modules were broken for months because nothing ever imported them.
417
+ `tests/test_imports.py` imports every module in the package, so a module that
418
+ nothing else happens to import still has to be importable.
414
419
 
415
420
  ## Licence
416
421
 
@@ -1,8 +1,8 @@
1
1
  # corekit
2
2
 
3
3
  Shared foundations for Python projects: structured logging, benchmarking,
4
- registries, FastAPI routers with built-in handlers, an in-memory record store,
5
- and ETL scaffolding.
4
+ registries, a FastAPI application with routers and handlers, SQL statements and
5
+ migrations, background tasks, an in-memory record store, and ETL scaffolding.
6
6
 
7
7
  Requires Python 3.11+.
8
8
 
@@ -12,17 +12,15 @@ Requires Python 3.11+.
12
12
  pip install python-corekit
13
13
  ```
14
14
 
15
- No credentials, no SSH key, no token — which means a project that depends on
16
- corekit can be cloned and built by anyone, including inside a Docker build.
15
+ The distribution is `python-corekit`; the import is `corekit`.
17
16
 
18
17
  Every dependency corekit needs is installed with it. There are no optional
19
18
  extras to remember, and no import that fails because something was left out.
20
19
 
21
- The distribution is `python-corekit`; the import is `corekit`. Pin a compatible
22
- release rather than tracking whatever is newest:
20
+ Pin a compatible release rather than tracking whatever is newest:
23
21
 
24
22
  ```
25
- python-corekit~=0.1.0
23
+ python-corekit~=0.2.0
26
24
  ```
27
25
 
28
26
  Before 1.0, the minor version carries breaking changes.
@@ -55,6 +53,57 @@ class Report(Benchmarkable):
55
53
  self.timing("queried") # logs the time since the previous split
56
54
  ```
57
55
 
56
+ ## Assembling an application
57
+
58
+ `Application` is a plain `FastAPI` subclass — every constructor argument,
59
+ including `lifespan`, passes straight through. What it adds is a short set of
60
+ assembly steps, each of which logs what it did.
61
+
62
+ ```python
63
+ from corekit.api import Application, Lifespan
64
+ from app.backend import routers
65
+
66
+ lifespan = Lifespan()
67
+ lifespan.add("cache", startup=cache.connect, shutdown=cache.disconnect)
68
+
69
+ app = Application(lifespan=lifespan)
70
+ app.discover_routers(routers)
71
+ ```
72
+
73
+ `discover_routers` imports every module under the package so each router can
74
+ register itself. It raises if it finds none: passing a package is a statement
75
+ that routers live there, and an app that silently serves nothing is worse than
76
+ one that refuses to start.
77
+
78
+ Lifespan steps start in the order added and shut down in reverse, the way
79
+ nested `with` blocks unwind. If a step fails on the way up, the steps that
80
+ already started are still torn down. A shutdown hook runs only if its own
81
+ startup completed, so it may assume the state that startup builds — and a hook
82
+ that raises cannot stop the unwind.
83
+
84
+ ### Middleware
85
+
86
+ Middleware order is a security property: a host check that reads a client
87
+ address before the proxy-header layer has rewritten it is checking the proxy,
88
+ not the client. So middleware is never auto-discovered, and corekit installs
89
+ none by default. A stack is declared in one place, outermost first — the order
90
+ a request actually meets the layers.
91
+
92
+ ```python
93
+ from corekit.api import MiddlewareStack
94
+
95
+ stack = (
96
+ MiddlewareStack()
97
+ .add(ProxyHeadersMiddleware, trusted_hosts="*")
98
+ .add(TrustedHostMiddleware, allowed_hosts=HOSTS)
99
+ .add(CORSMiddleware, allow_origins=ORIGINS, allow_credentials=True)
100
+ )
101
+ app.add_middleware_stack(stack)
102
+ ```
103
+
104
+ Reading top to bottom gives the order a request travels, which is the property
105
+ you need when reviewing it.
106
+
58
107
  ## Routers and handlers
59
108
 
60
109
  A router and the handler holding its business logic travel together. Declare the
@@ -114,113 +163,67 @@ people.get_record("Ada").age # O(1) lookup by id
114
163
 
115
164
  Stores pickle cleanly, including their dynamically generated record class.
116
165
 
117
- ## Homelab pieces
166
+ ## SQL statements
118
167
 
119
- ### Container control
168
+ Select, insert, update and delete are objects you build and then execute, so a
169
+ statement can be assembled in pieces and passed around before it runs.
120
170
 
121
171
  ```python
122
- from corekit.docker import Watchdog
172
+ from corekit.connections.sql import SQLConnection, Select, Insert, Update, Delete
173
+ from corekit.data import Field
123
174
 
124
- watchdog = Watchdog(enforce_label=True)
125
- watchdog.restart_container_by_name("minecraft")
126
- watchdog.find_and_stop(label="app", value="staging")
127
- ```
175
+ conn = SQLConnection("sqlite:///app.db")
128
176
 
129
- `enforce_label` limits the blast radius: with it on, only containers carrying
130
- the `watchdog=true` label can be started, stopped or paused, so a mistyped name
131
- cannot take down something unrelated. Leave it on unless the watchdog is meant
132
- to control everything on the host.
133
-
134
- ### Reacting to logs
135
-
136
- Describe what to watch for and what to do about it:
137
-
138
- ```yaml
139
- # config.yaml
140
- containers:
141
- - name: "minecraft-.*"
142
- rules:
143
- - name: "out of memory"
144
- pattern: "java.lang.OutOfMemoryError"
145
- severity: critical
146
- send_notification: true
147
- actions:
148
- - type: restart_container
149
- max_restarts: 3
150
- restart_window: 3600
151
- advanced:
152
- ignore_patterns:
153
- - "healthcheck"
154
- rate_limits:
155
- restart_container:
156
- count: 5
157
- period: hour
158
- ```
177
+ conn.execute(Insert(table=User, rows=[{"name": "Ada", "age": 36}]).add(name="Bob", age=17))
159
178
 
160
- ```python
161
- from corekit.log_monitor import LogMonitor
179
+ adults = conn.fetch(Select(table=User).where(User.age >= 18).order_by(User.name).limit(10))
162
180
 
163
- LogMonitor.run("config.yaml")
181
+ conn.execute(Update(table=User).where(User.name == "Bob").set(age=18))
182
+ conn.execute(Delete(table=User).where(User.age < 13))
164
183
  ```
165
184
 
166
- Restarts are capped per container, so a crash loop cannot become a restart loop.
167
-
168
- ### Notifications
185
+ `where` accepts a SQLAlchemy expression or a `corekit.data` one, so the same
186
+ predicate language that filters a `Dataset` also filters a table:
169
187
 
170
188
  ```python
171
- from corekit.notifications import BaseNotificationService, Notification, NotificationType
172
-
173
- class DiscordNotifier(BaseNotificationService):
174
- """
175
- Sends notifications to a Discord channel.
176
- """
177
-
178
- def _send(self, message: str) -> None:
179
- discord.post(message)
180
-
181
- notifier.notify(Notification(message="disk full", type=NotificationType.ERROR))
189
+ Select(table=User).where(Field("age") >= 18)
182
190
  ```
183
191
 
184
- Override `_send`, not `send`. `notify()` formats the message and calls `_send`,
185
- so an override with any other name is silently ignored.
192
+ Update and delete compile to a single statement rather than fetching rows and
193
+ looping, which matters most over a network, where fetch-and-loop pays a round
194
+ trip per row.
186
195
 
187
- ### Real-time updates
196
+ A `Delete` or `Update` with no condition raises rather than running:
188
197
 
189
- Publish from wherever the work happens:
190
-
191
- ```python
192
- from corekit.events import EventPublisher
193
-
194
- publisher = EventPublisher.for_resource("minecraft", "server", "survival")
195
- publisher.publish("backup_finished", {"size": "4.2GB"})
198
+ ```
199
+ Delete on User needs a condition; use truncate to empty a table
196
200
  ```
197
201
 
198
- Stream it to the browser:
202
+ ## Background tasks
199
203
 
200
- ```python
201
- from corekit.api import SSEResponse
202
- from corekit.events import SSEStream
204
+ Tasks describe work and register themselves by name. Nothing here imports a
205
+ queue library, so the same task runs under RQ, Celery, a cron entry, or a test
206
+ with no queue at all.
203
207
 
204
- @router.get("/events")
205
- async def events(channel: str) -> SSEResponse:
206
- return SSEResponse(SSEStream(channel, keepalive_interval=15))
207
- ```
208
+ ```python
209
+ from corekit.jobs import Task, run_task
210
+ from corekit.utils import encode_payload
208
211
 
209
- The browser side is three lines, and reconnects on its own:
212
+ class SendDigest(Task):
213
+ def task_function(self, user: str) -> None:
214
+ ...
210
215
 
211
- ```javascript
212
- const source = new EventSource("/events?channel=minecraft:server:survival");
213
- source.addEventListener("backup_finished", e => console.log(JSON.parse(e.data)));
216
+ # a worker, holding only a name and a JSON string
217
+ run_task("SendDigest", encode_payload(["ada"]))
214
218
  ```
215
219
 
216
- `SSEStream` sends a `connected` frame on subscribe, an optional `initial_state`
217
- so a client arriving late renders immediately, and a comment frame every
218
- `keepalive_interval` seconds so proxies do not close an idle connection. For
219
- WebSockets, `WebSocketBridge` relays the same channel and stops on a terminal
220
- status.
220
+ Arguments cross the queue as JSON, never as a serialized object. `pickle` and
221
+ `dill` execute code while loading, so a queue holding objects turns write
222
+ access to the queue into code execution in a worker. `encode_payload` and
223
+ `decode_payload` live in `corekit.utils`, since crossing a process boundary as
224
+ data is not specific to queues.
221
225
 
222
- Publishing never raises: an event that cannot be delivered should not take down
223
- the operation that produced it. `publish` returns whether it worked.
226
+ `ScheduledTask` adds an `interval` for tasks a scheduler should repeat.
224
227
 
225
228
  ## Parallel work
226
229
 
@@ -336,23 +339,25 @@ Imports go downward only.
336
339
 
337
340
  ```
338
341
  corekit/
339
- config.py constants.py
342
+ config/ settings, sources, loader
343
+ constants.py
340
344
 
341
345
  exceptions/ error types
342
346
 
343
347
  observability/ Loggable, Benchmarkable, Timer
344
- registry/ schemas/ utils/ SmartRegistry, enums and fields, helpers
348
+ registry/ schemas/ utils/ registries, enums and fields, helpers
345
349
  data/ Dataset and its filter expressions
350
+ jobs/ queue-independent background tasks
346
351
  crypto/ files/ serialization/
347
352
  concurrency/ ThreadLocalRegistry, ThreadWorker
348
353
  decorators/
349
354
 
350
355
  connections/ the Connectable lifecycle and @connect
351
- sql/ SQLConnection, queries, migrations
356
+ sql/ SQLConnection, statements, migrations
352
357
  redis/ RedisConnection
353
358
  http/ BaseApiClient, retries, responses
354
359
 
355
- api/ handlers, routers, responses
360
+ api/ Application, lifespan, middleware, routers
356
361
  docker/ notifications/ etl/
357
362
 
358
363
  events/ log_monitor/ built on the capabilities above
@@ -369,13 +374,13 @@ layering deliberately.
369
374
  ## Development
370
375
 
371
376
  ```bash
372
- pip install -e ".[dev,all]"
377
+ pip install -e ".[dev]"
373
378
  pytest
374
379
  ruff format . && ruff check --fix .
375
380
  ```
376
381
 
377
- `tests/test_imports.py` imports every module in the package. Keep it passing:
378
- several modules were broken for months because nothing ever imported them.
382
+ `tests/test_imports.py` imports every module in the package, so a module that
383
+ nothing else happens to import still has to be importable.
379
384
 
380
385
  ## Licence
381
386
 
@@ -0,0 +1,24 @@
1
+ """
2
+ FastAPI building blocks: an application, its lifespan, handlers, routers and responses.
3
+ """
4
+
5
+ from corekit.api.application import Application
6
+ from corekit.api.handler import BaseHandler
7
+ from corekit.api.lifespan import Lifespan, LifespanStep
8
+ from corekit.api.middleware import MiddlewareLayer, MiddlewareStack
9
+ from corekit.api.responses import SSEResponse
10
+ from corekit.api.routers import CatchAllRouter, SimpleRouter, SmartRouter, router_registry
11
+
12
+ __all__ = [
13
+ "Application",
14
+ "BaseHandler",
15
+ "CatchAllRouter",
16
+ "Lifespan",
17
+ "LifespanStep",
18
+ "MiddlewareLayer",
19
+ "MiddlewareStack",
20
+ "SSEResponse",
21
+ "SimpleRouter",
22
+ "SmartRouter",
23
+ "router_registry",
24
+ ]