python-corekit 0.4.1__tar.gz → 0.5.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 (235) hide show
  1. {python_corekit-0.4.1/python_corekit.egg-info → python_corekit-0.5.0}/PKG-INFO +8 -7
  2. {python_corekit-0.4.1 → python_corekit-0.5.0}/README.md +5 -5
  3. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/connection.py +6 -2
  4. python_corekit-0.5.0/corekit/html/__init__.py +22 -0
  5. python_corekit-0.5.0/corekit/html/client.py +22 -0
  6. python_corekit-0.5.0/corekit/html/models.py +20 -0
  7. python_corekit-0.5.0/corekit/html/parser.py +272 -0
  8. python_corekit-0.5.0/corekit/html/scraper.py +127 -0
  9. python_corekit-0.5.0/corekit/html/settings.py +197 -0
  10. python_corekit-0.5.0/corekit/html/state.py +56 -0
  11. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/__init__.py +2 -1
  12. python_corekit-0.5.0/corekit/http/api.py +92 -0
  13. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/client.py +24 -54
  14. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/response.py +38 -5
  15. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/client.py +3 -2
  16. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/tools/loop.py +5 -2
  17. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/wire.py +11 -5
  18. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/__init__.py +8 -1
  19. python_corekit-0.5.0/corekit/schemas/models/arbitrary.py +55 -0
  20. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/__init__.py +48 -7
  21. python_corekit-0.5.0/corekit/utils/coercion.py +104 -0
  22. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/collections.py +136 -3
  23. python_corekit-0.5.0/corekit/utils/parsers.py +205 -0
  24. {python_corekit-0.4.1 → python_corekit-0.5.0}/pyproject.toml +3 -2
  25. {python_corekit-0.4.1 → python_corekit-0.5.0/python_corekit.egg-info}/PKG-INFO +8 -7
  26. {python_corekit-0.4.1 → python_corekit-0.5.0}/python_corekit.egg-info/SOURCES.txt +11 -0
  27. {python_corekit-0.4.1 → python_corekit-0.5.0}/python_corekit.egg-info/requires.txt +1 -0
  28. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_architecture.py +1 -0
  29. python_corekit-0.5.0/tests/test_html.py +214 -0
  30. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_llm.py +48 -0
  31. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_requests.py +19 -3
  32. python_corekit-0.5.0/tests/test_schemas_arbitrary.py +55 -0
  33. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_sql.py +2 -1
  34. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_utils_coercion.py +47 -1
  35. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_utils_collections.py +85 -1
  36. python_corekit-0.5.0/tests/test_utils_parsers.py +238 -0
  37. python_corekit-0.4.1/corekit/http/api.py +0 -24
  38. python_corekit-0.4.1/corekit/schemas/models/arbitrary.py +0 -11
  39. python_corekit-0.4.1/corekit/utils/coercion.py +0 -118
  40. {python_corekit-0.4.1 → python_corekit-0.5.0}/LICENSE +0 -0
  41. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/__init__.py +0 -0
  42. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/__init__.py +0 -0
  43. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/application.py +0 -0
  44. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/handler.py +0 -0
  45. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/lifespan.py +0 -0
  46. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/middleware.py +0 -0
  47. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/responses.py +0 -0
  48. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/api/routers.py +0 -0
  49. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/concurrency/__init__.py +0 -0
  50. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/concurrency/decorators.py +0 -0
  51. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/concurrency/thread_local.py +0 -0
  52. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/concurrency/worker.py +0 -0
  53. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/config/__init__.py +0 -0
  54. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/config/loader.py +0 -0
  55. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/config/settings.py +0 -0
  56. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/config/sources.py +0 -0
  57. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/__init__.py +0 -0
  58. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/connectable.py +0 -0
  59. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/decorators.py +0 -0
  60. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/redis/__init__.py +0 -0
  61. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/redis/connection.py +0 -0
  62. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/registry.py +0 -0
  63. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/__init__.py +0 -0
  64. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/fields/__init__.py +0 -0
  65. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/fields/jsonb.py +0 -0
  66. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/migration/__init__.py +0 -0
  67. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/migration/base.py +0 -0
  68. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/migration/operations.py +0 -0
  69. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/migration/registry.py +0 -0
  70. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/migration/table.py +0 -0
  71. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/operations/__init__.py +0 -0
  72. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/operations/base.py +0 -0
  73. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/operations/statements.py +0 -0
  74. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/query.py +0 -0
  75. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/connections/sql/table.py +0 -0
  76. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/crypto/__init__.py +0 -0
  77. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/crypto/constants.py +0 -0
  78. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/crypto/enum.py +0 -0
  79. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/crypto/hasher.py +0 -0
  80. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/__init__.py +0 -0
  81. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/dataset.py +0 -0
  82. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/expressions/__init__.py +0 -0
  83. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/expressions/comparison.py +0 -0
  84. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/expressions/expression.py +0 -0
  85. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/expressions/operator.py +0 -0
  86. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/expressions/target.py +0 -0
  87. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/record.py +0 -0
  88. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/data/stats.py +0 -0
  89. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/decorators/__init__.py +0 -0
  90. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/decorators/exception_handling.py +0 -0
  91. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/decorators/warnings.py +0 -0
  92. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/docker/__init__.py +0 -0
  93. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/docker/watchdog.py +0 -0
  94. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/__init__.py +0 -0
  95. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/connection.py +0 -0
  96. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/extract/__init__.py +0 -0
  97. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/extract/extractor.py +0 -0
  98. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/extract/schemas.py +0 -0
  99. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/load/__init__.py +0 -0
  100. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/load/loader.py +0 -0
  101. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/load/schemas.py +0 -0
  102. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/orchestrator.py +0 -0
  103. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/schemas.py +0 -0
  104. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/transform/__init__.py +0 -0
  105. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/transform/schemas.py +0 -0
  106. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/etl/transform/transformer.py +0 -0
  107. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/__init__.py +0 -0
  108. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/enum.py +0 -0
  109. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/frames.py +0 -0
  110. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/models.py +0 -0
  111. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/publisher.py +0 -0
  112. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/reader.py +0 -0
  113. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/sse.py +0 -0
  114. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/events/websocket.py +0 -0
  115. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/__init__.py +0 -0
  116. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/actions.py +0 -0
  117. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/asgi.py +0 -0
  118. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/constants.py +0 -0
  119. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/exceptionator.py +0 -0
  120. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/fingerprint.py +0 -0
  121. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/guard.py +0 -0
  122. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/jobs.py +0 -0
  123. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptionator/models.py +0 -0
  124. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptions/__init__.py +0 -0
  125. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptions/base.py +0 -0
  126. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptions/custom/__init__.py +0 -0
  127. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptions/enum.py +0 -0
  128. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptions/http/__init__.py +0 -0
  129. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/exceptions/types.py +0 -0
  130. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/files/__init__.py +0 -0
  131. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/files/base.py +0 -0
  132. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/files/enum.py +0 -0
  133. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/files/json.py +0 -0
  134. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/files/toml.py +0 -0
  135. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/exceptions.py +0 -0
  136. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/exponential_backoff.py +0 -0
  137. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/status.py +0 -0
  138. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/http/stream.py +0 -0
  139. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/jobs/__init__.py +0 -0
  140. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/jobs/registry.py +0 -0
  141. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/jobs/runner.py +0 -0
  142. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/jobs/task.py +0 -0
  143. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/__init__.py +0 -0
  144. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/enum.py +0 -0
  145. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/events.py +0 -0
  146. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/messages.py +0 -0
  147. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/prompts/__init__.py +0 -0
  148. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/prompts/enum.py +0 -0
  149. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/prompts/exceptions.py +0 -0
  150. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/prompts/loader.py +0 -0
  151. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/prompts/template.py +0 -0
  152. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/protocols.py +0 -0
  153. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/streaming.py +0 -0
  154. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/tools/__init__.py +0 -0
  155. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/tools/base.py +0 -0
  156. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/tools/detection.py +0 -0
  157. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/llm/tools/registry.py +0 -0
  158. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/log_monitor/__init__.py +0 -0
  159. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/log_monitor/constants.py +0 -0
  160. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/log_monitor/models.py +0 -0
  161. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/log_monitor/service.py +0 -0
  162. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/notifications/__init__.py +0 -0
  163. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/notifications/base.py +0 -0
  164. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/notifications/models.py +0 -0
  165. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/__init__.py +0 -0
  166. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/benchmarkable.py +0 -0
  167. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/loggable.py +0 -0
  168. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/request_context.py +0 -0
  169. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/timing/__init__.py +0 -0
  170. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/timing/constants.py +0 -0
  171. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/timing/split.py +0 -0
  172. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/observability/timing/timer.py +0 -0
  173. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/py.typed +0 -0
  174. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/registry/__init__.py +0 -0
  175. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/registry/ordered.py +0 -0
  176. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/registry/registry.py +0 -0
  177. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/dataclasses/__init__.py +0 -0
  178. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/enum.py +0 -0
  179. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/models/__init__.py +0 -0
  180. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/models/date_models.py +0 -0
  181. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/pydantic/__init__.py +0 -0
  182. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/pydantic/fields.py +0 -0
  183. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/types.py +0 -0
  184. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/schemas/version.py +0 -0
  185. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/serialization/__init__.py +0 -0
  186. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/serialization/enum.py +0 -0
  187. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/serialization/pickle_file.py +0 -0
  188. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/serialization/serializable.py +0 -0
  189. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/serialization/serializer.py +0 -0
  190. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/ids.py +0 -0
  191. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/payload.py +0 -0
  192. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/raise_exc.py +0 -0
  193. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/text.py +0 -0
  194. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/time.py +0 -0
  195. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/validators.py +0 -0
  196. {python_corekit-0.4.1 → python_corekit-0.5.0}/corekit/utils/void.py +0 -0
  197. {python_corekit-0.4.1 → python_corekit-0.5.0}/python_corekit.egg-info/dependency_links.txt +0 -0
  198. {python_corekit-0.4.1 → python_corekit-0.5.0}/python_corekit.egg-info/top_level.txt +0 -0
  199. {python_corekit-0.4.1 → python_corekit-0.5.0}/setup.cfg +0 -0
  200. {python_corekit-0.4.1 → python_corekit-0.5.0}/setup.py +0 -0
  201. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_api.py +0 -0
  202. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_application.py +0 -0
  203. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_concurrency.py +0 -0
  204. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_config.py +0 -0
  205. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_connections.py +0 -0
  206. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_data.py +0 -0
  207. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_docker.py +0 -0
  208. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_etl.py +0 -0
  209. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_events.py +0 -0
  210. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_exceptionator.py +0 -0
  211. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_exceptions.py +0 -0
  212. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_expressions.py +0 -0
  213. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_expressions_sqlalchemy.py +0 -0
  214. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_files.py +0 -0
  215. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_imports.py +0 -0
  216. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_imports_are_top_level.py +0 -0
  217. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_jobs.py +0 -0
  218. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_lifespan.py +0 -0
  219. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_log_monitor.py +0 -0
  220. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_middleware_stack.py +0 -0
  221. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_migration.py +0 -0
  222. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_notifications.py +0 -0
  223. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_observability.py +0 -0
  224. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_operations.py +0 -0
  225. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_ordered_registry.py +0 -0
  226. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_redis.py +0 -0
  227. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_registry.py +0 -0
  228. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_request_context.py +0 -0
  229. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_schemas_enum.py +0 -0
  230. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_serialization.py +0 -0
  231. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_utils.py +0 -0
  232. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_utils_ids.py +0 -0
  233. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_utils_payload.py +0 -0
  234. {python_corekit-0.4.1 → python_corekit-0.5.0}/tests/test_utils_text.py +0 -0
  235. {python_corekit-0.4.1 → python_corekit-0.5.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.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
3
+ Version: 0.5.0
4
+ Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, ETL, LLM chat/tool primitives, and HTML scraping
5
5
  Author: Steven Jacobsen
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/stevejaker/corekit
@@ -23,6 +23,7 @@ Requires-Dist: sqlmodel<0.1,>=0.0.16
23
23
  Requires-Dist: SQLAlchemy<3,>=2.0
24
24
  Requires-Dist: redis<7,>=5.0
25
25
  Requires-Dist: httpx<1,>=0.27
26
+ Requires-Dist: selectolax<1,>=0.3
26
27
  Requires-Dist: docker<8,>=7.0
27
28
  Requires-Dist: PyYAML<7,>=6.0
28
29
  Requires-Dist: dill<0.5,>=0.3.8
@@ -347,13 +348,13 @@ class GithubClient(BaseApiClient):
347
348
  return "https://api.github.com"
348
349
 
349
350
  response = GithubClient().get("/users/octocat")
350
- response.data["login"]
351
+ response.data.login
351
352
  ```
352
353
 
353
- Retries 429 and 5xx with exponential backoff. Every response is a
354
- `BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
355
- raising. The same methods are awaitable inside a running event loop:
356
- `await client.get(...)`.
354
+ Retries 429 and 5xx with exponential backoff. ``BaseHttpClient`` returns a
355
+ ``BaseHttpResponse`` (text + metadata); ``BaseApiClient`` adds structured
356
+ ``data`` from JSON (``None`` when the body is not JSON). The same methods are
357
+ awaitable inside a running event loop: `await client.get(...)`.
357
358
 
358
359
  ## Serialization
359
360
 
@@ -311,13 +311,13 @@ class GithubClient(BaseApiClient):
311
311
  return "https://api.github.com"
312
312
 
313
313
  response = GithubClient().get("/users/octocat")
314
- response.data["login"]
314
+ response.data.login
315
315
  ```
316
316
 
317
- Retries 429 and 5xx with exponential backoff. Every response is a
318
- `BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
319
- raising. The same methods are awaitable inside a running event loop:
320
- `await client.get(...)`.
317
+ Retries 429 and 5xx with exponential backoff. ``BaseHttpClient`` returns a
318
+ ``BaseHttpResponse`` (text + metadata); ``BaseApiClient`` adds structured
319
+ ``data`` from JSON (``None`` when the body is not JSON). The same methods are
320
+ awaitable inside a running event loop: `await client.get(...)`.
321
321
 
322
322
  ## Serialization
323
323
 
@@ -330,15 +330,19 @@ class SQLConnection(Connectable):
330
330
  """
331
331
  return list(self._exec(query.build()))
332
332
 
333
- def delete_many_by_query(self, query: Query) -> None:
333
+ def delete_many_by_query(self, query: Query) -> int:
334
334
  """
335
335
  Delete every row matching a Query.
336
+
337
+ Returns the number of rows deleted.
336
338
  """
337
339
  self.require_writable()
338
340
  self._start_session()
339
- for item in self.fetch_many_by_query(query):
341
+ items = self.fetch_many_by_query(query)
342
+ for item in items:
340
343
  self.session.delete(item)
341
344
  self.session.commit()
345
+ return len(items)
342
346
 
343
347
  def execute(self, operation: Operation) -> Any:
344
348
  """
@@ -0,0 +1,22 @@
1
+ """
2
+ HTML parsing and crawling helpers.
3
+ """
4
+
5
+ from corekit.html.client import WebscrapeClient
6
+ from corekit.html.models import LinkItem, Page, PageMetadata
7
+ from corekit.html.parser import HtmlParser
8
+ from corekit.html.scraper import Scraper
9
+ from corekit.html.settings import CleanSettings, LinkFilterSettings
10
+ from corekit.html.state import ScraperState
11
+
12
+ __all__ = [
13
+ "CleanSettings",
14
+ "HtmlParser",
15
+ "LinkFilterSettings",
16
+ "LinkItem",
17
+ "Page",
18
+ "PageMetadata",
19
+ "Scraper",
20
+ "ScraperState",
21
+ "WebscrapeClient",
22
+ ]
@@ -0,0 +1,22 @@
1
+ from typing import Any
2
+
3
+ from corekit.http import BaseHttpClient
4
+
5
+
6
+ class WebscrapeClient(BaseHttpClient):
7
+ """
8
+ HTTP client with browser-like default headers for scraping.
9
+
10
+ Inherits GET-only ``supported_methods`` from ``BaseHttpClient``.
11
+ """
12
+
13
+ def __init__(self, **kwargs: Any) -> None:
14
+ kwargs.setdefault("strict", False)
15
+ super().__init__(**kwargs)
16
+
17
+ @property
18
+ def headers(self) -> dict[str, str]:
19
+ return {
20
+ "Accept": "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8",
21
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0",
22
+ }
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass(slots=True)
5
+ class PageMetadata:
6
+ title: str = ""
7
+ base_url: str | None = None
8
+ meta: dict[str, str] = field(default_factory=dict)
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class LinkItem:
13
+ href: str
14
+ text: str | None = None
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class Page:
19
+ metadata: PageMetadata
20
+ content: str
@@ -0,0 +1,272 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterator
4
+ from urllib.parse import urldefrag, urljoin, urlparse, urlunparse
5
+
6
+ from selectolax.lexbor import LexborHTMLParser
7
+
8
+ from corekit.exceptions import InternalCoreException
9
+ from corekit.exceptions.enum import Retryability
10
+ from corekit.html.models import LinkItem, PageMetadata
11
+ from corekit.html.settings import CleanSettings, LinkFilterSettings
12
+ from corekit.observability import Loggable
13
+ from corekit.utils import iterator, parse_string, safe_dict, safe_string
14
+
15
+ _SKIP_SCHEMES: frozenset[str] = frozenset({"", "javascript", "mailto", "tel", "data", "blob", "about"})
16
+
17
+
18
+ class HtmlParser(Loggable):
19
+ def __init__(
20
+ self,
21
+ html: str | bytes,
22
+ url: str,
23
+ *,
24
+ clean_settings: CleanSettings | None = None,
25
+ link_settings: LinkFilterSettings | None = None,
26
+ ) -> None:
27
+ super().__init__()
28
+ self._html = html
29
+ self._url = url
30
+ self._clean_settings = clean_settings or CleanSettings.defaults()
31
+ self._link_settings = link_settings
32
+ self._original = LexborHTMLParser(html)
33
+ self._current = LexborHTMLParser(html)
34
+
35
+ def reset(self) -> None:
36
+ """
37
+ Resets the current working tree back to the pristine original state
38
+ """
39
+ self._current = LexborHTMLParser(self._html)
40
+
41
+ def clone(self) -> HtmlParser:
42
+ """
43
+ Returns a deep-copy independent instance of the parser
44
+ """
45
+ return HtmlParser(
46
+ self._html,
47
+ self._url,
48
+ clean_settings=self._clean_settings,
49
+ link_settings=self._link_settings,
50
+ )
51
+
52
+ @property
53
+ def current(self) -> LexborHTMLParser:
54
+ """
55
+ Direct access to the underlying active LexborHTMLParser instance
56
+ """
57
+ return self._current
58
+
59
+ def _html_text(self) -> str | None:
60
+ parsed_html = parse_string(self._current.html)
61
+ return parsed_html.value or None
62
+
63
+ def _has_content(self) -> bool:
64
+ html = self._html_text()
65
+ if not html:
66
+ return False
67
+ body = self._current.body
68
+ if body is None:
69
+ return bool(html)
70
+ text = safe_string(body.text(separator=" ", strip=True))
71
+ return bool(text)
72
+
73
+ def get_clean_content(self) -> str:
74
+ """
75
+ Clean the page with tiered fallbacks and return non-empty HTML.
76
+
77
+ Tries user/effective settings first (ideal → no-scope → emergency).
78
+ If those yield no content and settings were customized, falls back to
79
+ built-in defaults and logs an error. Raises if every attempt fails.
80
+ """
81
+ content = self._clean_with_settings(self._clean_settings)
82
+ if content is not None:
83
+ return content
84
+
85
+ if self._clean_settings.is_customized():
86
+ fallback = self._clean_with_settings(CleanSettings.defaults())
87
+ if fallback is not None:
88
+ self.error(
89
+ f"User clean settings for {self._url!r} yielded no content; "
90
+ f"fell back to defaults ({len(fallback)} chars remaining)"
91
+ )
92
+ return fallback
93
+
94
+ raise InternalCoreException(
95
+ f"Failed to extract clean content from {self._url!r}",
96
+ retryable=Retryability.NON_RETRYABLE,
97
+ error="all cleaning tiers produced empty content",
98
+ )
99
+
100
+ def _clean_with_settings(self, settings: CleanSettings) -> str | None:
101
+ for tier in ("ideal", "fallback", "emergency"):
102
+ self.reset()
103
+ self._apply_tier(settings, tier)
104
+ if self._has_content():
105
+ html = self._html_text()
106
+ if html is not None:
107
+ return html
108
+ self.warning(f"[HtmlParser] Clean tier {tier!r} produced no content for {self._url!r}")
109
+ return None
110
+
111
+ def _apply_tier(self, settings: CleanSettings, tier: str) -> None:
112
+ if tier == "ideal":
113
+ self.prune_noise(settings, emergency=False)
114
+ self.apply_remove_selectors(settings)
115
+ self.scope_to_content_root(settings)
116
+ self.sanitize_attributes(settings)
117
+ elif tier == "fallback":
118
+ self.prune_noise(settings, emergency=False)
119
+ self.apply_remove_selectors(settings)
120
+ self.sanitize_attributes(settings)
121
+ else:
122
+ self.prune_noise(settings, emergency=True)
123
+ self.apply_remove_selectors(settings)
124
+ self.sanitize_attributes(settings)
125
+
126
+ def get_metadata(self) -> PageMetadata:
127
+ """
128
+ Extracts and validates page metadata into a structured model
129
+ """
130
+ title_node = self._current.css_first("title")
131
+ page_title = title_node.text(strip=True) if title_node else ""
132
+
133
+ base_node = self._current.css_first("base")
134
+ base_url = base_node.attributes.get("href") if base_node and base_node.attributes else None
135
+
136
+ meta_tags: dict[str, str] = {}
137
+ for m in iterator(self._current.css("meta"), where=lambda node: bool(node.attributes)):
138
+ attrs = safe_dict(m.attributes)
139
+ key = attrs.get("name") or attrs.get("property")
140
+ content = attrs.get("content")
141
+ if key and content:
142
+ meta_tags[key] = content
143
+
144
+ return PageMetadata(
145
+ title=page_title,
146
+ base_url=base_url,
147
+ meta=meta_tags,
148
+ )
149
+
150
+ def _base_for_links(self) -> str:
151
+ metadata = self.get_metadata()
152
+ if metadata.base_url:
153
+ return urljoin(self._url, metadata.base_url)
154
+ return self._url
155
+
156
+ def resolve_href(self, href: str, *, base: str | None = None) -> str | None:
157
+ """
158
+ Resolve a raw href to an absolute http(s) URL, or None if it should be skipped.
159
+
160
+ Fragments are always stripped. Query strings are kept unless
161
+ ``link_settings.should_strip_query`` says otherwise for the URL.
162
+ """
163
+ raw = href.strip()
164
+ if not raw or raw.startswith("#"):
165
+ return None
166
+
167
+ absolute = urljoin(base or self._base_for_links(), raw)
168
+ absolute, _frag = urldefrag(absolute)
169
+ parsed = urlparse(absolute)
170
+ scheme = parsed.scheme.lower()
171
+ if scheme in _SKIP_SCHEMES or scheme not in {"http", "https"}:
172
+ return None
173
+ if self._link_settings and self._link_settings.should_strip_query(absolute) and parsed.query:
174
+ absolute = urlunparse(parsed._replace(query=""))
175
+ return absolute
176
+
177
+ def get_links(self) -> Iterator[LinkItem]:
178
+ """
179
+ Extracts links as absolute URLs with anchor text.
180
+ """
181
+ base = self._base_for_links()
182
+
183
+ def has_href(node: Any) -> bool:
184
+ return bool(safe_dict(node.attributes).get("href"))
185
+
186
+ for a in iterator(self._current.css("a[href]"), where=has_href):
187
+ resolved = self.resolve_href(a.attributes["href"], base=base)
188
+ if not resolved:
189
+ continue
190
+ yield LinkItem(href=resolved, text=parse_string(a.text(strip=True)).value)
191
+
192
+ def find_content_root(self, settings: CleanSettings | None = None) -> Any:
193
+ """
194
+ Navigates to and returns the optimal starting content container node
195
+ """
196
+ effective = settings or self._clean_settings
197
+ selector = effective.effective_content_selector(self._url)
198
+ if selector:
199
+ if node := self._current.css_first(selector):
200
+ return node
201
+ return self._current.css_first("body")
202
+
203
+ def prune_noise(self, settings: CleanSettings | None = None, *, emergency: bool = False) -> None:
204
+ """
205
+ Removes unhelpful structural elements while optionally preserving search UI
206
+ """
207
+ effective = settings or self._clean_settings
208
+ for tag in effective.effective_noise_tags(emergency=emergency):
209
+ for node in self._current.css(tag):
210
+ node.remove()
211
+
212
+ if emergency or not effective.preserve_header_nav_with_search:
213
+ return
214
+
215
+ for container in iterator(
216
+ self._current.css("header, nav"),
217
+ where=lambda _node: not _node.css_first(effective.search_selectors),
218
+ ):
219
+ container.remove()
220
+
221
+ def apply_remove_selectors(self, settings: CleanSettings | None = None) -> None:
222
+ """
223
+ Remove nodes matching user-provided CSS selectors.
224
+ """
225
+ effective = settings or self._clean_settings
226
+ for selector in effective.remove_selectors:
227
+ for node in self._current.css(selector):
228
+ node.remove()
229
+
230
+ def sanitize_attributes(self, settings: CleanSettings | None = None) -> None:
231
+ """
232
+ Strips non-allowed attributes across the tree
233
+ """
234
+ effective = settings or self._clean_settings
235
+ allowed = effective.effective_allowed_attributes()
236
+ for node in iterator(self._current.css("*"), where=lambda n: bool(n.attributes)):
237
+ for key in iterator(
238
+ list(node.attrs.keys()),
239
+ where=lambda attr: attr.lower() not in allowed,
240
+ ):
241
+ del node.attrs[key]
242
+
243
+ def scope_to_content_root(self, settings: CleanSettings | None = None) -> None:
244
+ """
245
+ Replace the working tree with the best content container, when found.
246
+ """
247
+ effective = settings or self._clean_settings
248
+ root = self.find_content_root(effective)
249
+ if root is None:
250
+ self.warning(f"[HtmlParser] No content root found for {self._url!r}")
251
+ return
252
+
253
+ html = safe_string(root.html)
254
+ if not html:
255
+ self.warning(f"[HtmlParser] Content root for {self._url!r} had empty HTML")
256
+ return
257
+
258
+ text = safe_string(root.text(separator=" ", strip=True))
259
+ if not text:
260
+ self.warning(f"[HtmlParser] Content root for {self._url!r} had no text content")
261
+ return
262
+
263
+ self._current = LexborHTMLParser(html)
264
+
265
+ def clean(self, settings: CleanSettings | None = None) -> None:
266
+ """
267
+ Ideal-path clean: prune, remove selectors, scope, sanitize.
268
+
269
+ Prefer ``get_clean_content`` when you need tiered fallbacks.
270
+ """
271
+ effective = settings or self._clean_settings
272
+ self._apply_tier(effective, "ideal")
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from http import HTTPMethod
5
+ from typing import Any, AsyncIterator, cast
6
+
7
+ import httpx
8
+
9
+ from corekit.html.client import WebscrapeClient
10
+ from corekit.html.models import Page
11
+ from corekit.html.parser import HtmlParser
12
+ from corekit.html.settings import CleanSettings, LinkFilterSettings
13
+ from corekit.html.state import ScraperState
14
+ from corekit.http import BaseHttpResponse
15
+ from corekit.observability import Benchmarkable
16
+ from corekit.utils import iterator
17
+
18
+
19
+ class Scraper(Benchmarkable):
20
+ def __init__(
21
+ self,
22
+ start_urls: list[str],
23
+ settings: LinkFilterSettings,
24
+ *,
25
+ clean_settings: CleanSettings | None = None,
26
+ concurrency: int = 10,
27
+ request_timeout: float = 10.0,
28
+ retries: int = 2,
29
+ ) -> None:
30
+ super().__init__()
31
+ self.concurrency = concurrency
32
+ self.semaphore = asyncio.Semaphore(concurrency)
33
+ self.timeout = request_timeout
34
+ self.retries = retries
35
+ self.settings = settings
36
+ self.clean_settings = clean_settings or CleanSettings.defaults()
37
+
38
+ self._limits = httpx.Limits(
39
+ max_keepalive_connections=concurrency,
40
+ max_connections=concurrency * 2,
41
+ )
42
+ self._http: httpx.AsyncClient | None = None
43
+ self._client: WebscrapeClient | None = None
44
+ self._state = ScraperState(start_urls)
45
+
46
+ def initialize(self) -> None:
47
+ self._http = httpx.AsyncClient(
48
+ limits=self._limits,
49
+ timeout=self.timeout,
50
+ follow_redirects=True,
51
+ )
52
+ self._client = WebscrapeClient(
53
+ http_client=self._http,
54
+ timeout=self.timeout,
55
+ retries=self.retries,
56
+ base_delay=0.5,
57
+ )
58
+
59
+ async def close(self) -> None:
60
+ if self._http is not None:
61
+ await self._http.aclose()
62
+ self._http = None
63
+ self._client = None
64
+
65
+ async def __aenter__(self) -> Scraper:
66
+ self.initialize()
67
+ return self
68
+
69
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
70
+ await self.close()
71
+
72
+ async def _fetch(self, url: str) -> BaseHttpResponse:
73
+ if self._client is None:
74
+ raise RuntimeError("Scraper must be initialized before running.")
75
+ response = await self._client.request(HTTPMethod.GET, url)
76
+ response.raise_for_status()
77
+ return response
78
+
79
+ async def _worker(self, worker_id: int) -> None:
80
+ while True:
81
+ url = await self._state.get_next_url()
82
+ try:
83
+ async with self.semaphore:
84
+ self.info(f"[Worker {worker_id}] Fetching: {url}")
85
+ response = await self._fetch(url)
86
+
87
+ page_url = response.url or url
88
+ tree = HtmlParser(
89
+ response.text,
90
+ page_url,
91
+ clean_settings=self.clean_settings,
92
+ link_settings=self.settings,
93
+ )
94
+ for link in iterator(tree.get_links(), where=lambda item: self.settings.should_visit(item.href)):
95
+ await self._state.enqueue(link.href)
96
+
97
+ if self.settings.should_scrape(url) or self.settings.should_scrape(page_url):
98
+ page = Page(
99
+ metadata=tree.get_metadata(),
100
+ content=tree.get_clean_content(),
101
+ )
102
+ await self._state.record_output(page)
103
+
104
+ except Exception as e:
105
+ self.error(f"[Worker {worker_id}] Error on {url}: {e}")
106
+
107
+ finally:
108
+ self._state.task_done()
109
+
110
+ async def run(self) -> AsyncIterator[Page]:
111
+ if not self._client:
112
+ raise RuntimeError("Scraper must be initialized before running.")
113
+
114
+ workers = [asyncio.create_task(self._worker(i)) for i in range(self.concurrency)]
115
+ monitor_task = asyncio.create_task(self._state.join())
116
+
117
+ try:
118
+ while True:
119
+ result = await self._state.get_output()
120
+ if result is ScraperState.FINISHED_TOKEN:
121
+ break
122
+ yield cast(Page, result)
123
+ self._state.output_task_done()
124
+ finally:
125
+ monitor_task.cancel()
126
+ for w in workers:
127
+ w.cancel()