krons 0.2.3__tar.gz → 0.2.4__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 (277) hide show
  1. krons-0.2.4/CHANGELOG.md +38 -0
  2. {krons-0.2.3 → krons-0.2.4}/PKG-INFO +1 -1
  3. {krons-0.2.3 → krons-0.2.4}/pyproject.toml +1 -1
  4. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/common.py +1 -1
  5. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/prepare_msg.py +1 -2
  6. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/node.py +27 -26
  7. krons-0.2.4/src/krons/core/specs/adapters/_utils.py +87 -0
  8. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/catalog/_content.py +6 -0
  9. {krons-0.2.3 → krons-0.2.4}/src/krons/core/types/db_types.py +103 -18
  10. {krons-0.2.3 → krons-0.2.4}/src/krons/session/session.py +3 -1
  11. {krons-0.2.3 → krons-0.2.4}/tests/specs/test_catalog.py +10 -0
  12. {krons-0.2.3 → krons-0.2.4}/tests/types/test_db_types.py +117 -1
  13. {krons-0.2.3 → krons-0.2.4}/uv.lock +1 -1
  14. krons-0.2.3/src/krons/core/specs/adapters/_utils.py +0 -45
  15. {krons-0.2.3 → krons-0.2.4}/.github/workflows/release.yml +0 -0
  16. {krons-0.2.3 → krons-0.2.4}/.gitignore +0 -0
  17. {krons-0.2.3 → krons-0.2.4}/.python-version +0 -0
  18. {krons-0.2.3 → krons-0.2.4}/CLAUDE.md +0 -0
  19. {krons-0.2.3 → krons-0.2.4}/LICENSE +0 -0
  20. {krons-0.2.3 → krons-0.2.4}/README.md +0 -0
  21. {krons-0.2.3 → krons-0.2.4}/cookbooks/007_fan_out_in.py +0 -0
  22. {krons-0.2.3 → krons-0.2.4}/cookbooks/test_conduct.py +0 -0
  23. {krons-0.2.3 → krons-0.2.4}/examples/__init__.py +0 -0
  24. {krons-0.2.3 → krons-0.2.4}/examples/code_review_panel.py +0 -0
  25. {krons-0.2.3 → krons-0.2.4}/examples/codegen_pipeline.py +0 -0
  26. {krons-0.2.3 → krons-0.2.4}/examples/dynamic_response.py +0 -0
  27. {krons-0.2.3 → krons-0.2.4}/examples/event_sourcing.py +0 -0
  28. {krons-0.2.3 → krons-0.2.4}/examples/multi_agent_orchestration.py +0 -0
  29. {krons-0.2.3 → krons-0.2.4}/examples/pipeline_router.py +0 -0
  30. {krons-0.2.3 → krons-0.2.4}/examples/research_agent.py +0 -0
  31. {krons-0.2.3 → krons-0.2.4}/examples/tech_debate.py +0 -0
  32. {krons-0.2.3 → krons-0.2.4}/examples/validation_loop.py +0 -0
  33. {krons-0.2.3 → krons-0.2.4}/src/krons/__init__.py +0 -0
  34. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/__init__.py +0 -0
  35. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/mcps/__init__.py +0 -0
  36. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/mcps/loader.py +0 -0
  37. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/mcps/wrapper.py +0 -0
  38. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/__init__.py +0 -0
  39. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/action.py +0 -0
  40. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/assistant.py +0 -0
  41. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/instruction.py +0 -0
  42. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/role.py +0 -0
  43. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/message/system.py +0 -0
  44. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/__init__.py +0 -0
  45. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/act.py +0 -0
  46. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/generate.py +0 -0
  47. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/llm_reparse.py +0 -0
  48. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/operate.py +0 -0
  49. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/parse.py +0 -0
  50. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/react.py +0 -0
  51. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/specs.py +0 -0
  52. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/structure.py +0 -0
  53. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/operations/utils.py +0 -0
  54. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/providers/__init__.py +0 -0
  55. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/providers/anthropic_messages.py +0 -0
  56. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/providers/claude_code.py +0 -0
  57. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/providers/gemini.py +0 -0
  58. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/providers/match.py +0 -0
  59. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/providers/oai_chat.py +0 -0
  60. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/third_party/__init__.py +0 -0
  61. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/third_party/anthropic_models.py +0 -0
  62. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/third_party/claude_code.py +0 -0
  63. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/third_party/gemini_models.py +0 -0
  64. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/third_party/openai_models.py +0 -0
  65. {krons-0.2.3 → krons-0.2.4}/src/krons/agent/tool.py +0 -0
  66. {krons-0.2.3 → krons-0.2.4}/src/krons/core/__init__.py +0 -0
  67. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/__init__.py +0 -0
  68. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/broadcaster.py +0 -0
  69. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/element.py +0 -0
  70. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/event.py +0 -0
  71. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/eventbus.py +0 -0
  72. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/flow.py +0 -0
  73. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/graph.py +0 -0
  74. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/pile.py +0 -0
  75. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/processor.py +0 -0
  76. {krons-0.2.3 → krons-0.2.4}/src/krons/core/base/progression.py +0 -0
  77. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/__init__.py +0 -0
  78. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/adapters/__init__.py +0 -0
  79. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/adapters/dataclass_field.py +0 -0
  80. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/adapters/factory.py +0 -0
  81. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/adapters/pydantic_adapter.py +0 -0
  82. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/adapters/sql_ddl.py +0 -0
  83. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/catalog/__init__.py +0 -0
  84. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/catalog/_audit.py +0 -0
  85. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/catalog/_common.py +0 -0
  86. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/catalog/_enforcement.py +0 -0
  87. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/factory.py +0 -0
  88. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/operable.py +0 -0
  89. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/protocol.py +0 -0
  90. {krons-0.2.3 → krons-0.2.4}/src/krons/core/specs/spec.py +0 -0
  91. {krons-0.2.3 → krons-0.2.4}/src/krons/core/types/__init__.py +0 -0
  92. {krons-0.2.3 → krons-0.2.4}/src/krons/core/types/_sentinel.py +0 -0
  93. {krons-0.2.3 → krons-0.2.4}/src/krons/core/types/base.py +0 -0
  94. {krons-0.2.3 → krons-0.2.4}/src/krons/core/types/identity.py +0 -0
  95. {krons-0.2.3 → krons-0.2.4}/src/krons/errors.py +0 -0
  96. {krons-0.2.3 → krons-0.2.4}/src/krons/protocols.py +0 -0
  97. {krons-0.2.3 → krons-0.2.4}/src/krons/py.typed +0 -0
  98. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/__init__.py +0 -0
  99. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/backend.py +0 -0
  100. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/endpoint.py +0 -0
  101. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/hook.py +0 -0
  102. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/imodel.py +0 -0
  103. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/registry.py +0 -0
  104. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/utilities/__init__.py +0 -0
  105. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/utilities/header_factory.py +0 -0
  106. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/utilities/rate_limited_executor.py +0 -0
  107. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/utilities/rate_limiter.py +0 -0
  108. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/utilities/resilience.py +0 -0
  109. {krons-0.2.3 → krons-0.2.4}/src/krons/resource/utilities/token_calculator.py +0 -0
  110. {krons-0.2.3 → krons-0.2.4}/src/krons/session/__init__.py +0 -0
  111. {krons-0.2.3 → krons-0.2.4}/src/krons/session/constraints.py +0 -0
  112. {krons-0.2.3 → krons-0.2.4}/src/krons/session/exchange.py +0 -0
  113. {krons-0.2.3 → krons-0.2.4}/src/krons/session/message.py +0 -0
  114. {krons-0.2.3 → krons-0.2.4}/src/krons/session/registry.py +0 -0
  115. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/__init__.py +0 -0
  116. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_function_arg_parser.py +0 -0
  117. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_hash.py +0 -0
  118. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_json_dump.py +0 -0
  119. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_lazy_init.py +0 -0
  120. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_pythonic_function_call.py +0 -0
  121. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_to_list.py +0 -0
  122. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_to_num.py +0 -0
  123. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/_utils.py +0 -0
  124. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/__init__.py +0 -0
  125. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_async_call.py +0 -0
  126. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_cancel.py +0 -0
  127. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_errors.py +0 -0
  128. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_patterns.py +0 -0
  129. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_primitives.py +0 -0
  130. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_priority_queue.py +0 -0
  131. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_resource_tracker.py +0 -0
  132. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_run_async.py +0 -0
  133. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_task.py +0 -0
  134. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/concurrency/_utils.py +0 -0
  135. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/display.py +0 -0
  136. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/fuzzy/__init__.py +0 -0
  137. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/fuzzy/_extract_json.py +0 -0
  138. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/fuzzy/_fuzzy_json.py +0 -0
  139. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/fuzzy/_fuzzy_match.py +0 -0
  140. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/fuzzy/_string_similarity.py +0 -0
  141. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/fuzzy/_to_dict.py +0 -0
  142. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/schemas/__init__.py +0 -0
  143. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/schemas/_breakdown_pydantic_annotation.py +0 -0
  144. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/schemas/_formatter.py +0 -0
  145. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/schemas/_minimal_yaml.py +0 -0
  146. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/schemas/_typescript.py +0 -0
  147. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/sql/__init__.py +0 -0
  148. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/sql/_sql_validation.py +0 -0
  149. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/validators/__init__.py +0 -0
  150. {krons-0.2.3 → krons-0.2.4}/src/krons/utils/validators/_validate_image_url.py +0 -0
  151. {krons-0.2.3 → krons-0.2.4}/src/krons/work/__init__.py +0 -0
  152. {krons-0.2.3 → krons-0.2.4}/src/krons/work/engine.py +0 -0
  153. {krons-0.2.3 → krons-0.2.4}/src/krons/work/form.py +0 -0
  154. {krons-0.2.3 → krons-0.2.4}/src/krons/work/operations/__init__.py +0 -0
  155. {krons-0.2.3 → krons-0.2.4}/src/krons/work/operations/builder.py +0 -0
  156. {krons-0.2.3 → krons-0.2.4}/src/krons/work/operations/context.py +0 -0
  157. {krons-0.2.3 → krons-0.2.4}/src/krons/work/operations/flow.py +0 -0
  158. {krons-0.2.3 → krons-0.2.4}/src/krons/work/operations/node.py +0 -0
  159. {krons-0.2.3 → krons-0.2.4}/src/krons/work/operations/registry.py +0 -0
  160. {krons-0.2.3 → krons-0.2.4}/src/krons/work/report.py +0 -0
  161. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/__init__.py +0 -0
  162. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/__init__.py +0 -0
  163. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/boolean.py +0 -0
  164. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/choice.py +0 -0
  165. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/mapping.py +0 -0
  166. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/model.py +0 -0
  167. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/number.py +0 -0
  168. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/common/string.py +0 -0
  169. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/registry.py +0 -0
  170. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/rule.py +0 -0
  171. {krons-0.2.3 → krons-0.2.4}/src/krons/work/rules/validator.py +0 -0
  172. {krons-0.2.3 → krons-0.2.4}/src/krons/work/worker.py +0 -0
  173. {krons-0.2.3 → krons-0.2.4}/tests/__init__.py +0 -0
  174. {krons-0.2.3 → krons-0.2.4}/tests/agent/__init__.py +0 -0
  175. {krons-0.2.3 → krons-0.2.4}/tests/agent/test_mcps_unit.py +0 -0
  176. {krons-0.2.3 → krons-0.2.4}/tests/agent/test_message.py +0 -0
  177. {krons-0.2.3 → krons-0.2.4}/tests/agent/test_providers_e2e.py +0 -0
  178. {krons-0.2.3 → krons-0.2.4}/tests/agent/test_providers_unit.py +0 -0
  179. {krons-0.2.3 → krons-0.2.4}/tests/agent/test_tool_unit.py +0 -0
  180. {krons-0.2.3 → krons-0.2.4}/tests/conftest.py +0 -0
  181. {krons-0.2.3 → krons-0.2.4}/tests/core/__init__.py +0 -0
  182. {krons-0.2.3 → krons-0.2.4}/tests/core/test_broadcaster.py +0 -0
  183. {krons-0.2.3 → krons-0.2.4}/tests/core/test_element.py +0 -0
  184. {krons-0.2.3 → krons-0.2.4}/tests/core/test_error_paths.py +0 -0
  185. {krons-0.2.3 → krons-0.2.4}/tests/core/test_event.py +0 -0
  186. {krons-0.2.3 → krons-0.2.4}/tests/core/test_event_status_race.py +0 -0
  187. {krons-0.2.3 → krons-0.2.4}/tests/core/test_eventbus.py +0 -0
  188. {krons-0.2.3 → krons-0.2.4}/tests/core/test_flow.py +0 -0
  189. {krons-0.2.3 → krons-0.2.4}/tests/core/test_flow_edge_cases.py +0 -0
  190. {krons-0.2.3 → krons-0.2.4}/tests/core/test_graph.py +0 -0
  191. {krons-0.2.3 → krons-0.2.4}/tests/core/test_graph_event_loop.py +0 -0
  192. {krons-0.2.3 → krons-0.2.4}/tests/core/test_node.py +0 -0
  193. {krons-0.2.3 → krons-0.2.4}/tests/core/test_pile.py +0 -0
  194. {krons-0.2.3 → krons-0.2.4}/tests/core/test_pile_edge_cases.py +0 -0
  195. {krons-0.2.3 → krons-0.2.4}/tests/core/test_processor.py +0 -0
  196. {krons-0.2.3 → krons-0.2.4}/tests/core/test_processor_security.py +0 -0
  197. {krons-0.2.3 → krons-0.2.4}/tests/core/test_progression.py +0 -0
  198. {krons-0.2.3 → krons-0.2.4}/tests/core/test_progression_edge_cases.py +0 -0
  199. {krons-0.2.3 → krons-0.2.4}/tests/core/test_serialization_roundtrip.py +0 -0
  200. {krons-0.2.3 → krons-0.2.4}/tests/core/test_thread_safety_stress.py +0 -0
  201. {krons-0.2.3 → krons-0.2.4}/tests/operations/__init__.py +0 -0
  202. {krons-0.2.3 → krons-0.2.4}/tests/operations/test_builder.py +0 -0
  203. {krons-0.2.3 → krons-0.2.4}/tests/operations/test_flow.py +0 -0
  204. {krons-0.2.3 → krons-0.2.4}/tests/operations/test_node.py +0 -0
  205. {krons-0.2.3 → krons-0.2.4}/tests/operations/test_op_flow.py +0 -0
  206. {krons-0.2.3 → krons-0.2.4}/tests/operations/test_op_node.py +0 -0
  207. {krons-0.2.3 → krons-0.2.4}/tests/operations/test_registry.py +0 -0
  208. {krons-0.2.3 → krons-0.2.4}/tests/resource/__init__.py +0 -0
  209. {krons-0.2.3 → krons-0.2.4}/tests/resource/test_backend.py +0 -0
  210. {krons-0.2.3 → krons-0.2.4}/tests/resource/test_endpoint.py +0 -0
  211. {krons-0.2.3 → krons-0.2.4}/tests/resource/test_hook.py +0 -0
  212. {krons-0.2.3 → krons-0.2.4}/tests/resource/test_imodel.py +0 -0
  213. {krons-0.2.3 → krons-0.2.4}/tests/resource/test_registry.py +0 -0
  214. {krons-0.2.3 → krons-0.2.4}/tests/resource/utilities/__init__.py +0 -0
  215. {krons-0.2.3 → krons-0.2.4}/tests/resource/utilities/test_header_factory.py +0 -0
  216. {krons-0.2.3 → krons-0.2.4}/tests/resource/utilities/test_rate_limiter.py +0 -0
  217. {krons-0.2.3 → krons-0.2.4}/tests/resource/utilities/test_resilience.py +0 -0
  218. {krons-0.2.3 → krons-0.2.4}/tests/resource/utilities/test_resilience_retry.py +0 -0
  219. {krons-0.2.3 → krons-0.2.4}/tests/rules/__init__.py +0 -0
  220. {krons-0.2.3 → krons-0.2.4}/tests/rules/test_base.py +0 -0
  221. {krons-0.2.3 → krons-0.2.4}/tests/rules/test_builtin.py +0 -0
  222. {krons-0.2.3 → krons-0.2.4}/tests/rules/test_registry.py +0 -0
  223. {krons-0.2.3 → krons-0.2.4}/tests/rules/test_rule_params_edge_cases.py +0 -0
  224. {krons-0.2.3 → krons-0.2.4}/tests/rules/test_rules_comprehensive.py +0 -0
  225. {krons-0.2.3 → krons-0.2.4}/tests/rules/test_validator.py +0 -0
  226. {krons-0.2.3 → krons-0.2.4}/tests/session/__init__.py +0 -0
  227. {krons-0.2.3 → krons-0.2.4}/tests/session/test_exchange.py +0 -0
  228. {krons-0.2.3 → krons-0.2.4}/tests/session/test_message.py +0 -0
  229. {krons-0.2.3 → krons-0.2.4}/tests/session/test_message_edge_cases.py +0 -0
  230. {krons-0.2.3 → krons-0.2.4}/tests/session/test_session.py +0 -0
  231. {krons-0.2.3 → krons-0.2.4}/tests/session/test_session_edge_cases.py +0 -0
  232. {krons-0.2.3 → krons-0.2.4}/tests/session/test_session_logging.py +0 -0
  233. {krons-0.2.3 → krons-0.2.4}/tests/specs/__init__.py +0 -0
  234. {krons-0.2.3 → krons-0.2.4}/tests/specs/test_factory.py +0 -0
  235. {krons-0.2.3 → krons-0.2.4}/tests/test_protocols.py +0 -0
  236. {krons-0.2.3 → krons-0.2.4}/tests/types/__init__.py +0 -0
  237. {krons-0.2.3 → krons-0.2.4}/tests/types/conftest.py +0 -0
  238. {krons-0.2.3 → krons-0.2.4}/tests/types/spec_adapters/__init__.py +0 -0
  239. {krons-0.2.3 → krons-0.2.4}/tests/types/spec_adapters/test_adapters_py311.py +0 -0
  240. {krons-0.2.3 → krons-0.2.4}/tests/types/spec_adapters/test_protocol.py +0 -0
  241. {krons-0.2.3 → krons-0.2.4}/tests/types/spec_adapters/test_pydantic_field.py +0 -0
  242. {krons-0.2.3 → krons-0.2.4}/tests/types/spec_adapters/test_sql_ddl_specs.py +0 -0
  243. {krons-0.2.3 → krons-0.2.4}/tests/types/test_identity.py +0 -0
  244. {krons-0.2.3 → krons-0.2.4}/tests/types/test_model.py +0 -0
  245. {krons-0.2.3 → krons-0.2.4}/tests/types/test_operable.py +0 -0
  246. {krons-0.2.3 → krons-0.2.4}/tests/types/test_sentinel.py +0 -0
  247. {krons-0.2.3 → krons-0.2.4}/tests/types/test_spec.py +0 -0
  248. {krons-0.2.3 → krons-0.2.4}/tests/types/test_types.py +0 -0
  249. {krons-0.2.3 → krons-0.2.4}/tests/types/test_types_py311.py +0 -0
  250. {krons-0.2.3 → krons-0.2.4}/tests/utils/__init__.py +0 -0
  251. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/__init__.py +0 -0
  252. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_async_call.py +0 -0
  253. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_cancel.py +0 -0
  254. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_errors.py +0 -0
  255. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_patterns.py +0 -0
  256. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_primitives.py +0 -0
  257. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_priority_queue.py +0 -0
  258. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_run_async.py +0 -0
  259. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_task.py +0 -0
  260. {krons-0.2.3 → krons-0.2.4}/tests/utils/concurrency/test_utils.py +0 -0
  261. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_extract_json.py +0 -0
  262. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_fuzzy_json.py +0 -0
  263. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_fuzzy_match.py +0 -0
  264. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_hash.py +0 -0
  265. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_json_dump.py +0 -0
  266. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_lazy_init.py +0 -0
  267. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_sql_validation.py +0 -0
  268. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_string_similarity.py +0 -0
  269. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_to_dict.py +0 -0
  270. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_to_list.py +0 -0
  271. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_to_num.py +0 -0
  272. {krons-0.2.3 → krons-0.2.4}/tests/utils/test_utils.py +0 -0
  273. {krons-0.2.3 → krons-0.2.4}/tests/work/__init__.py +0 -0
  274. {krons-0.2.3 → krons-0.2.4}/tests/work/test_engine.py +0 -0
  275. {krons-0.2.3 → krons-0.2.4}/tests/work/test_form.py +0 -0
  276. {krons-0.2.3 → krons-0.2.4}/tests/work/test_report.py +0 -0
  277. {krons-0.2.3 → krons-0.2.4}/tests/work/test_worker.py +0 -0
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.4] - 2026-02-04
9
+
10
+ ### Added
11
+
12
+ - `parse_forward_ref` canonical parser for ForwardRef annotations
13
+ - Support for `FK["Model"]` and `FK['Model']` string forward references
14
+ - Robust nullability detection: `Optional[X]`, `Union[X, None]`, `X | None`
15
+ - `meta_key` parameter in `ContentSpecs.get_specs()` for DB alias customization
16
+ - Duck typing (`_is_spec_like`) to avoid circular imports
17
+
18
+ ### Changed
19
+
20
+ - DDL generation now uses config-driven audit columns (dict pattern)
21
+ - `_utils.py` uses shared `parse_forward_ref` instead of duplicate implementation
22
+ - Removed `include_audit_columns` parameter from `generate_ddl()` (now config-driven)
23
+
24
+ ### Fixed
25
+
26
+ - ForwardRef parsing with `from __future__ import annotations` (PEP 563)
27
+ - Nested Union types in nullability detection (e.g., `Union[FK[User], None]`)
28
+
29
+ ## [0.2.3] - 2026-02-04
30
+
31
+ ### Added
32
+
33
+ - Multi-agent example patterns (code_review_panel, tech_debate)
34
+ - 10 comprehensive examples demonstrating framework capabilities
35
+
36
+ ### Fixed
37
+
38
+ - Pile UUID access bug for Element retrieval
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: krons
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Spec-based composable framework for building type-safe systems
5
5
  Project-URL: Homepage, https://github.com/khive-ai/krons
6
6
  Project-URL: Repository, https://github.com/khive-ai/krons
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "krons"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Spec-based composable framework for building type-safe systems"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2,7 +2,7 @@ from typing import Any, Protocol, runtime_checkable
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
- from krons.core.types import Enum, MaybeUnset, Unset
5
+ from krons.core.types import Enum
6
6
 
7
7
 
8
8
  @runtime_checkable
@@ -1,8 +1,7 @@
1
1
  # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- from collections.abc import Callable
5
- from typing import TYPE_CHECKING, Any, Literal, cast
4
+ from typing import Any, cast
6
5
 
7
6
  from pydantic import JsonValue
8
7
 
@@ -745,9 +745,12 @@ def create_node(
745
745
  has_embedding = True
746
746
 
747
747
  # 1. Build all possible specs
748
+ # Extract meta_key from config_kwargs if provided, else use default
749
+ meta_key = config_kwargs.get("meta_key", "node_metadata")
748
750
  all_specs = ContentSpecs.get_specs(
749
751
  content_type=content if content else Unset,
750
752
  dim=resolved_embedding_dim,
753
+ meta_key=meta_key,
751
754
  ) + AuditSpecs.get_specs(use_uuid=True)
752
755
 
753
756
  # 2. Track which fields to include
@@ -837,19 +840,17 @@ def _extract_base_type(annotation: Any) -> Any:
837
840
  return annotation
838
841
 
839
842
 
840
- def generate_ddl(
841
- node_cls: type[Node],
842
- *,
843
- include_audit_columns: bool = True,
844
- ) -> str:
843
+ def generate_ddl(node_cls: type[Node]) -> str:
845
844
  """Generate CREATE TABLE DDL from Node subclass.
846
845
 
847
846
  Flattens content fields (if configured), adds audit columns, and
848
847
  generates PostgreSQL DDL with pgvector support for embeddings.
849
848
 
849
+ Audit column inclusion is driven by NodeConfig settings (track_updated_at,
850
+ soft_delete, versioning, etc.).
851
+
850
852
  Args:
851
853
  node_cls: Persistable Node subclass (must have table_name)
852
- include_audit_columns: Include audit columns from NodeConfig
853
854
 
854
855
  Returns:
855
856
  CREATE TABLE IF NOT EXISTS statement
@@ -875,7 +876,8 @@ def generate_ddl(
875
876
  )
876
877
 
877
878
  all_specs = ContentSpecs.get_specs(
878
- dim=config.embedding_dim if config.embedding_enabled else Unset
879
+ dim=config.embedding_dim if config.embedding_enabled else Unset,
880
+ meta_key=config.meta_key,
879
881
  ) + AuditSpecs.get_specs(use_uuid=True)
880
882
 
881
883
  # Flatten content: extract fields from BaseModel instead of generic JSONB
@@ -900,25 +902,24 @@ def generate_ddl(
900
902
  ):
901
903
  include.add("content")
902
904
 
903
- include.add("metadata")
904
-
905
- if include_audit_columns:
906
- if config.track_updated_at:
907
- include.add("updated_at")
908
- if config.track_updated_by:
909
- include.add("updated_by")
910
- if config.track_is_active:
911
- include.add("is_active")
912
- if config.soft_delete:
913
- include.update({"is_deleted", "deleted_at"})
914
- if config.track_deleted_by:
915
- include.add("deleted_by")
916
- if config.versioning:
917
- include.add("version")
918
- if config.content_hashing:
919
- include.add("content_hash")
920
- if config.integrity_hashing:
921
- include.add("integrity_hash")
905
+ if not config.is_sentinel_field("meta_key") and config.meta_key != "metadata":
906
+ include.add(config.meta_key)
907
+
908
+ audit_cols = {
909
+ "updated_at": config.track_updated_at,
910
+ "updated_by": config.track_updated_by,
911
+ "is_active": config.track_is_active,
912
+ "is_deleted": config.soft_delete,
913
+ "deleted_at": config.soft_delete,
914
+ "deleted_by": config.soft_delete and config.track_deleted_by,
915
+ "version": config.versioning,
916
+ "content_hash": config.content_hashing,
917
+ "integrity_hash": config.integrity_hashing,
918
+ }
919
+
920
+ for col, enabled in audit_cols.items():
921
+ if enabled:
922
+ include.add(col)
922
923
 
923
924
  # If flattened, include the extracted content field names
924
925
  if config.flatten_content and content_type is not None:
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import types
4
+ from functools import reduce
5
+ from typing import Annotated, Any, ForwardRef, Union, get_args, get_origin
6
+ from uuid import UUID
7
+
8
+
9
+ def _resolve_forward_ref(fwd: ForwardRef) -> dict[str, Any]:
10
+ """Handle ForwardRef annotations (from 'from __future__ import annotations').
11
+
12
+ Parses the string representation to extract type info for DDL generation.
13
+ FK[Model] -> Annotated[UUID, FKMeta(model_name)], Vector[dim] -> list[float], etc.
14
+
15
+ Uses parse_forward_ref from db_types as canonical parser.
16
+ """
17
+ from krons.core.types.db_types import parse_forward_ref
18
+
19
+ fk, vec, nullable = parse_forward_ref(fwd)
20
+
21
+ # FK[Model] -> Annotated[UUID, FKMeta]
22
+ if fk is not None:
23
+ base_type = Annotated[UUID, fk]
24
+ return {"base_type": base_type, "nullable": nullable, "listable": False}
25
+
26
+ # Vector[dim] -> Annotated[list[float], VectorMeta]
27
+ if vec is not None:
28
+ base_type = Annotated[list[float], vec]
29
+ return {"base_type": base_type, "nullable": nullable, "listable": False}
30
+
31
+ # Default: treat as generic type (will map to TEXT in SQL)
32
+ return {"base_type": str, "nullable": nullable, "listable": False}
33
+
34
+
35
+ def resolve_annotation_to_base_types(annotation: Any) -> dict[str, Any]:
36
+ """Resolve an annotation to its base types, detecting nullable and listable.
37
+
38
+ Args:
39
+ annotation: Type annotation to resolve (may include Optional, list, etc.)
40
+
41
+ Returns:
42
+ Dict with keys:
43
+ - base_type: The innermost type
44
+ - nullable: Whether None is allowed
45
+ - listable: Whether it's a list type
46
+ """
47
+ # Handle ForwardRef (from 'from __future__ import annotations')
48
+ if isinstance(annotation, ForwardRef):
49
+ return _resolve_forward_ref(annotation)
50
+
51
+ def resolve_nullable_inner_type(_anno: Any) -> tuple[bool, Any]:
52
+ origin = get_origin(_anno)
53
+
54
+ if origin is type(None):
55
+ return True, type(None)
56
+
57
+ if origin in (type(int | str), types.UnionType) or origin is Union:
58
+ args = get_args(_anno)
59
+ non_none_args = [a for a in args if a is not type(None)]
60
+ if len(args) != len(non_none_args):
61
+ if len(non_none_args) == 1:
62
+ return True, non_none_args[0]
63
+ if non_none_args:
64
+ return True, reduce(lambda a, b: a | b, non_none_args)
65
+ return False, _anno
66
+
67
+ return False, _anno
68
+
69
+ def resolve_listable_element_type(_anno: Any) -> Any:
70
+ origin = get_origin(_anno)
71
+
72
+ if origin is list:
73
+ args = get_args(_anno)
74
+ if args:
75
+ return True, args[0]
76
+ return True, Any
77
+
78
+ return False, _anno
79
+
80
+ _null, _inner = resolve_nullable_inner_type(annotation)
81
+ _list, _elem = resolve_listable_element_type(_inner)
82
+
83
+ return {
84
+ "base_type": _elem,
85
+ "nullable": _null,
86
+ "listable": _list,
87
+ }
@@ -33,12 +33,14 @@ class ContentSpecs(BaseModel):
33
33
  *,
34
34
  content_type: type | UnsetType = Unset,
35
35
  dim: int | UnsetType = Unset,
36
+ meta_key: str | UnsetType = Unset,
36
37
  ) -> list[Spec]:
37
38
  """Get list of content Specs.
38
39
 
39
40
  Args:
40
41
  content_type: Type for content/metadata fields (default: dict).
41
42
  dim: Embedding dimension. Unset = list[float], int = Vector[dim].
43
+ meta_key: DB alias for metadata field (e.g., "node_metadata").
42
44
  """
43
45
  operable = Operable.from_structure(cls)
44
46
  specs = {spec.name: spec for spec in operable.get_specs()}
@@ -48,6 +50,10 @@ class ContentSpecs(BaseModel):
48
50
  specs["content"] = Spec(content_type, name="content").as_nullable()
49
51
  specs["metadata"] = Spec(content_type, name="metadata").as_nullable()
50
52
 
53
+ # Add meta_key alias if specified (DB mode uses this to avoid SQL reserved word)
54
+ if meta_key is not Unset and isinstance(meta_key, str):
55
+ specs[meta_key] = Spec(dict[str, Any], name=meta_key).as_nullable()
56
+
51
57
  # Override embedding with vector dimension if specified
52
58
  if dim is not Unset and isinstance(dim, int):
53
59
  specs["embedding"] = Spec(
@@ -14,8 +14,9 @@ Extraction:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import re
17
18
  import types
18
- from typing import Annotated, Any, Literal, Union, get_args, get_origin
19
+ from typing import Annotated, Any, ForwardRef, Literal, Union, get_args, get_origin
19
20
  from uuid import UUID
20
21
 
21
22
  from krons.core.types._sentinel import Unset, UnsetType, not_sentinel
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "Vector",
33
34
  "VectorMeta",
34
35
  "extract_kron_db_meta",
36
+ "parse_forward_ref",
35
37
  ]
36
38
 
37
39
 
@@ -200,6 +202,88 @@ def _find_in_field_info(field_info: Any, meta_type: type) -> Any | None:
200
202
  return None
201
203
 
202
204
 
205
+ def _is_spec_like(obj: Any) -> bool:
206
+ """Check if object looks like a Spec (duck typing to avoid hard import)."""
207
+ return (
208
+ hasattr(obj, "get")
209
+ and hasattr(obj, "__class__")
210
+ and "Spec" in type(obj).__name__
211
+ )
212
+
213
+
214
+ def _detect_nullable(arg: str) -> bool:
215
+ """Detect nullability from annotation string.
216
+
217
+ Handles:
218
+ - X | None, None | X (PEP 604 union)
219
+ - Optional[X] (typing.Optional)
220
+ - Union[X, None], Union[None, X] (typing.Union with nested types)
221
+ """
222
+ # PEP 604 style: X | None or None | X
223
+ if re.search(r"\|\s*None\b", arg) or re.search(r"\bNone\s*\|", arg):
224
+ return True
225
+ # Optional[X]
226
+ if re.search(r"\bOptional\s*\[", arg):
227
+ return True
228
+ # Union[..., None, ...] - if Union is present and None is anywhere in string
229
+ # This handles nested types like Union[FK[User], None]
230
+ if re.search(r"\bUnion\s*\[", arg) and re.search(r"\bNone\b", arg):
231
+ return True
232
+ return False
233
+
234
+
235
+ def parse_forward_ref(
236
+ fwd: ForwardRef,
237
+ ) -> tuple[FKMeta | None, VectorMeta | None, bool]:
238
+ """Parse FK/Vector metadata and nullability from a ForwardRef string.
239
+
240
+ Canonical parser for ForwardRef annotations from 'from __future__ import annotations'.
241
+
242
+ Handles:
243
+ - FK[Model], FK["Model"], FK['Model'] (bare and string refs)
244
+ - Vector[1536] (dimension as int literal)
245
+ - Nullability: X | None, Optional[X], Union[X, None]
246
+
247
+ Args:
248
+ fwd: ForwardRef to parse
249
+
250
+ Returns:
251
+ Tuple of (fk_meta, vector_meta, is_nullable)
252
+ - fk_meta: FKMeta if FK[...] found, else None
253
+ - vector_meta: VectorMeta if Vector[dim] found, else None
254
+ - is_nullable: True if nullable pattern detected
255
+ """
256
+ arg = fwd.__forward_arg__
257
+ fk: FKMeta | None = None
258
+ vec: VectorMeta | None = None
259
+ nullable = _detect_nullable(arg)
260
+
261
+ # Match FK[ModelName] or FK["ModelName"] or FK['ModelName']
262
+ fk_match = re.search(r"FK\[(['\"]?)(\w+)\1\]", arg)
263
+ if fk_match:
264
+ model_name = fk_match.group(2)
265
+ fk = FKMeta(model_name)
266
+
267
+ # Match Vector[dim]
268
+ vec_match = re.search(r"Vector\[(\d+)\]", arg)
269
+ if vec_match:
270
+ dim = int(vec_match.group(1))
271
+ vec = VectorMeta(dim)
272
+
273
+ return fk, vec, nullable
274
+
275
+
276
+ def _extract_from_forward_ref(
277
+ fwd: ForwardRef,
278
+ ) -> tuple[FKMeta | UnsetType, VectorMeta | UnsetType]:
279
+ """Extract FK/Vector metadata from a ForwardRef (returns Unset for missing).
280
+
281
+ Wrapper around parse_forward_ref for extract_kron_db_meta compatibility.
282
+ """
283
+ fk, vec, _ = parse_forward_ref(fwd)
284
+ return (fk if fk is not None else Unset, vec if vec is not None else Unset)
285
+
286
+
203
287
  def extract_kron_db_meta(
204
288
  from_: Any,
205
289
  metas: Literal["FK", "Vector", "BOTH"] = "BOTH",
@@ -209,7 +293,7 @@ def extract_kron_db_meta(
209
293
  Unified extraction dispatching on source type:
210
294
  - FieldInfo: searches Pydantic metadata and annotation
211
295
  - type/annotation: searches Annotated/Union structure
212
- - Spec: reads spec metadata directly
296
+ - Spec: reads spec metadata directly (if available)
213
297
 
214
298
  Args:
215
299
  from_: FieldInfo, type annotation, or Spec instance
@@ -228,6 +312,10 @@ def extract_kron_db_meta(
228
312
  if metas in ("Vector", "BOTH"):
229
313
  vec = _find_in_field_info(from_, VectorMeta) or Unset
230
314
 
315
+ elif isinstance(from_, ForwardRef):
316
+ # Handle ForwardRef from 'from __future__ import annotations'
317
+ fk, vec = _extract_from_forward_ref(from_)
318
+
231
319
  elif get_origin(from_) is not None or isinstance(from_, type):
232
320
  # Raw type annotation
233
321
  if metas in ("FK", "BOTH"):
@@ -235,23 +323,20 @@ def extract_kron_db_meta(
235
323
  if metas in ("Vector", "BOTH"):
236
324
  vec = _find_in_annotation(from_, VectorMeta) or Unset
237
325
 
326
+ elif _is_spec_like(from_):
327
+ # Spec-like object (duck typed to avoid circular imports)
328
+ if metas in ("FK", "BOTH"):
329
+ fk_val = from_.get("as_fk", Unset)
330
+ if not_sentinel(fk_val, {"none"}) and isinstance(fk_val, FKMeta):
331
+ fk = fk_val
332
+ if metas in ("Vector", "BOTH"):
333
+ vec_val = from_.get("embedding", Unset)
334
+ if not_sentinel(vec_val, {"none"}) and isinstance(vec_val, VectorMeta):
335
+ vec = vec_val
238
336
  else:
239
- # Try Spec (lazy import to avoid circular)
240
- from krons.core.specs.spec import Spec
241
-
242
- if isinstance(from_, Spec):
243
- if metas in ("FK", "BOTH"):
244
- fk_val = from_.get("as_fk", Unset)
245
- if not_sentinel(fk_val, {"none"}) and isinstance(fk_val, FKMeta):
246
- fk = fk_val
247
- if metas in ("Vector", "BOTH"):
248
- vec_val = from_.get("embedding", Unset)
249
- if not_sentinel(vec_val, {"none"}) and isinstance(vec_val, VectorMeta):
250
- vec = vec_val
251
- else:
252
- raise TypeError(
253
- f"from_ must be FieldInfo, type annotation, or Spec, got {type(from_).__name__}"
254
- )
337
+ raise TypeError(
338
+ f"from_ must be FieldInfo, type annotation, or Spec, got {type(from_).__name__}"
339
+ )
255
340
 
256
341
  if metas == "FK":
257
342
  return fk
@@ -84,7 +84,9 @@ class Session(Element):
84
84
  default_factory=lambda: Flow(item_type=Message)
85
85
  )
86
86
  resources: ResourceRegistry = Field(default_factory=ResourceRegistry, exclude=True)
87
- operations: OperationRegistry = Field(default_factory=OperationRegistry, exclude=True)
87
+ operations: OperationRegistry = Field(
88
+ default_factory=OperationRegistry, exclude=True
89
+ )
88
90
  config: SessionConfig = Field(default_factory=SessionConfig)
89
91
  default_branch_id: UUID | None = None
90
92
 
@@ -108,6 +108,16 @@ class TestContentSpecs:
108
108
  assert "metadata" in names
109
109
  assert "embedding" in names
110
110
 
111
+ def test_get_specs_with_meta_key(self):
112
+ """ContentSpecs with meta_key should include the alias."""
113
+ specs = ContentSpecs.get_specs(meta_key="node_metadata")
114
+
115
+ assert isinstance(specs, list)
116
+ assert len(specs) == 6
117
+
118
+ names = [s.name for s in specs]
119
+ assert "node_metadata" in names
120
+
111
121
  def test_get_specs_id_is_uuid(self):
112
122
  """ID spec should be UUID type."""
113
123
  specs = ContentSpecs.get_specs()
@@ -3,13 +3,14 @@
3
3
 
4
4
  """Tests for database type annotations (FK, Vector)."""
5
5
 
6
- from typing import Annotated, get_args, get_origin
6
+ from typing import Annotated, ForwardRef, get_args, get_origin
7
7
  from uuid import UUID
8
8
 
9
9
  import pytest
10
10
  from pydantic import BaseModel, Field
11
11
 
12
12
  from krons.core.types import FK, FKMeta, Unset, Vector, VectorMeta, extract_kron_db_meta
13
+ from krons.core.types.db_types import parse_forward_ref
13
14
 
14
15
 
15
16
  class MockTenant:
@@ -236,3 +237,118 @@ class TestVectorMetaExtraction:
236
237
  meta = extract_kron_db_meta(field_info, metas="Vector")
237
238
 
238
239
  assert meta is Unset
240
+
241
+
242
+ class TestParseForwardRef:
243
+ """Tests for parse_forward_ref canonical parser."""
244
+
245
+ # --- FK patterns ---
246
+
247
+ def test_fk_bare_model(self):
248
+ """Should parse FK[Model]."""
249
+ fwd = ForwardRef("FK[User]")
250
+ fk, vec, nullable = parse_forward_ref(fwd)
251
+
252
+ assert fk is not None
253
+ assert fk.model == "User"
254
+ assert vec is None
255
+ assert not nullable
256
+
257
+ def test_fk_string_double_quotes(self):
258
+ """Should parse FK["Model"] with double quotes."""
259
+ fwd = ForwardRef('FK["User"]')
260
+ fk, vec, nullable = parse_forward_ref(fwd)
261
+
262
+ assert fk is not None
263
+ assert fk.model == "User"
264
+
265
+ def test_fk_string_single_quotes(self):
266
+ """Should parse FK['Model'] with single quotes."""
267
+ fwd = ForwardRef("FK['User']")
268
+ fk, vec, nullable = parse_forward_ref(fwd)
269
+
270
+ assert fk is not None
271
+ assert fk.model == "User"
272
+
273
+ # --- Vector patterns ---
274
+
275
+ def test_vector_dimension(self):
276
+ """Should parse Vector[1536]."""
277
+ fwd = ForwardRef("Vector[1536]")
278
+ fk, vec, nullable = parse_forward_ref(fwd)
279
+
280
+ assert fk is None
281
+ assert vec is not None
282
+ assert vec.dim == 1536
283
+ assert not nullable
284
+
285
+ # --- Nullability: PEP 604 style ---
286
+
287
+ def test_nullable_pipe_none_suffix(self):
288
+ """Should detect X | None."""
289
+ fwd = ForwardRef("FK[User] | None")
290
+ fk, vec, nullable = parse_forward_ref(fwd)
291
+
292
+ assert fk is not None
293
+ assert nullable
294
+
295
+ def test_nullable_none_pipe_prefix(self):
296
+ """Should detect None | X."""
297
+ fwd = ForwardRef("None | FK[User]")
298
+ fk, vec, nullable = parse_forward_ref(fwd)
299
+
300
+ assert fk is not None
301
+ assert nullable
302
+
303
+ # --- Nullability: Optional style ---
304
+
305
+ def test_nullable_optional(self):
306
+ """Should detect Optional[X]."""
307
+ fwd = ForwardRef("Optional[FK[User]]")
308
+ fk, vec, nullable = parse_forward_ref(fwd)
309
+
310
+ assert fk is not None
311
+ assert nullable
312
+
313
+ # --- Nullability: Union style ---
314
+
315
+ def test_nullable_union_none_last(self):
316
+ """Should detect Union[X, None]."""
317
+ fwd = ForwardRef("Union[FK[User], None]")
318
+ fk, vec, nullable = parse_forward_ref(fwd)
319
+
320
+ assert fk is not None
321
+ assert nullable
322
+
323
+ def test_nullable_union_none_first(self):
324
+ """Should detect Union[None, X]."""
325
+ fwd = ForwardRef("Union[None, FK[User]]")
326
+ fk, vec, nullable = parse_forward_ref(fwd)
327
+
328
+ assert fk is not None
329
+ assert nullable
330
+
331
+ # --- Non-nullable cases ---
332
+
333
+ def test_not_nullable_plain(self):
334
+ """Plain type should not be nullable."""
335
+ fwd = ForwardRef("FK[User]")
336
+ _, _, nullable = parse_forward_ref(fwd)
337
+ assert not nullable
338
+
339
+ def test_not_nullable_union_without_none(self):
340
+ """Union without None should not be nullable."""
341
+ fwd = ForwardRef("Union[FK[User], FK[Tenant]]")
342
+ _, _, nullable = parse_forward_ref(fwd)
343
+ assert not nullable
344
+
345
+ # --- Unknown patterns ---
346
+
347
+ def test_unknown_type_returns_none(self):
348
+ """Unknown type should return None for FK and Vector."""
349
+ fwd = ForwardRef("SomeRandomType")
350
+ fk, vec, nullable = parse_forward_ref(fwd)
351
+
352
+ assert fk is None
353
+ assert vec is None
354
+ assert not nullable
@@ -889,7 +889,7 @@ wheels = [
889
889
 
890
890
  [[package]]
891
891
  name = "krons"
892
- version = "0.2.2"
892
+ version = "0.2.3"
893
893
  source = { editable = "." }
894
894
  dependencies = [
895
895
  { name = "anyio" },
@@ -1,45 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import types
4
- from functools import reduce
5
- from typing import Any, Union, get_args, get_origin
6
-
7
-
8
- def resolve_annotation_to_base_types(annotation: Any) -> dict[str, Any]:
9
- def resolve_nullable_inner_type(_anno: Any) -> tuple[bool, Any]:
10
- origin = get_origin(_anno)
11
-
12
- if origin is type(None):
13
- return True, type(None)
14
-
15
- if origin in (type(int | str), types.UnionType) or origin is Union:
16
- args = get_args(_anno)
17
- non_none_args = [a for a in args if a is not type(None)]
18
- if len(args) != len(non_none_args):
19
- if len(non_none_args) == 1:
20
- return True, non_none_args[0]
21
- if non_none_args:
22
- return True, reduce(lambda a, b: a | b, non_none_args)
23
- return False, _anno
24
-
25
- return False, _anno
26
-
27
- def resolve_listable_element_type(_anno: Any) -> Any:
28
- origin = get_origin(_anno)
29
-
30
- if origin is list:
31
- args = get_args(_anno)
32
- if args:
33
- return True, args[0]
34
- return True, Any
35
-
36
- return False, _anno
37
-
38
- _null, _inner = resolve_nullable_inner_type(annotation)
39
- _list, _elem = resolve_listable_element_type(_inner)
40
-
41
- return {
42
- "base_type": _elem,
43
- "nullable": _null,
44
- "listable": _list,
45
- }
File without changes
File without changes
File without changes
File without changes
File without changes