inputlayer-client-dev 0.1.0.dev915__tar.gz → 0.1.0.dev919__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 (145) hide show
  1. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/PKG-INFO +10 -10
  2. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/README.md +9 -9
  3. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/pyproject.toml +2 -2
  4. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/__init__.py +0 -4
  5. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/migrations/__init__.py +0 -19
  6. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/migrations/cli.py +26 -1
  7. inputlayer_client_dev-0.1.0.dev919/src/inputlayer/migrations/errors.py +27 -0
  8. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/migrations/executor.py +15 -6
  9. inputlayer_client_dev-0.1.0.dev919/src/inputlayer/migrations/loader.py +135 -0
  10. inputlayer_client_dev-0.1.0.dev919/src/inputlayer/migrations/recorder.py +83 -0
  11. inputlayer_client_dev-0.1.0.dev919/src/inputlayer/migrations/writer.py +51 -0
  12. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_cli.py +45 -3
  13. inputlayer_client_dev-0.1.0.dev919/tests/test_loader.py +167 -0
  14. inputlayer_client_dev-0.1.0.dev919/tests/test_migration_safety.py +167 -0
  15. inputlayer_client_dev-0.1.0.dev919/tests/test_writer.py +129 -0
  16. inputlayer_client_dev-0.1.0.dev915/src/inputlayer/migrations/loader.py +0 -90
  17. inputlayer_client_dev-0.1.0.dev915/src/inputlayer/migrations/recorder.py +0 -44
  18. inputlayer_client_dev-0.1.0.dev915/src/inputlayer/migrations/writer.py +0 -208
  19. inputlayer_client_dev-0.1.0.dev915/tests/test_loader.py +0 -232
  20. inputlayer_client_dev-0.1.0.dev915/tests/test_writer.py +0 -169
  21. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/.gitignore +0 -0
  22. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/LICENSE +0 -0
  23. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/01_quickstart.py +0 -0
  24. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/02_social_network.py +0 -0
  25. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/03_rag_pipeline.py +0 -0
  26. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/04_ecommerce.py +0 -0
  27. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/05_rbac.py +0 -0
  28. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/06_realtime_dashboard.py +0 -0
  29. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/07_dataframe_etl.py +0 -0
  30. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/08_session_rules.py +0 -0
  31. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/09_access_control.py +0 -0
  32. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/10_migrations.py +0 -0
  33. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/__init__.py +0 -0
  34. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/_common.py +0 -0
  35. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex01_retriever.py +0 -0
  36. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex02_vector.py +0 -0
  37. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex03_tool.py +0 -0
  38. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex04_lcel_chain.py +0 -0
  39. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex05_kg_building.py +0 -0
  40. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex06_explainable_rag.py +0 -0
  41. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex07_multihop.py +0 -0
  42. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex08_memory.py +0 -0
  43. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex09_access_control.py +0 -0
  44. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex10_multi_agent.py +0 -0
  45. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex11_anomaly.py +0 -0
  46. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex12_hallucination.py +0 -0
  47. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex13_guardrails.py +0 -0
  48. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex14_graphrag.py +0 -0
  49. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex15_caching.py +0 -0
  50. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex16_recommendation.py +0 -0
  51. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/ex17_lineage.py +0 -0
  52. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langchain/runner.py +0 -0
  53. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/__init__.py +0 -0
  54. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/_common.py +0 -0
  55. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex01_reasoning_loop.py +0 -0
  56. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex02_investigation.py +0 -0
  57. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex03_human_in_loop.py +0 -0
  58. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex04_branching_pipeline.py +0 -0
  59. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex05_self_correcting.py +0 -0
  60. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex06_collaborative_planning.py +0 -0
  61. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex07_event_correlation.py +0 -0
  62. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex08_tool_selection.py +0 -0
  63. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex09_streaming_aggregation.py +0 -0
  64. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex10_resumable_graph.py +0 -0
  65. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex11_memory.py +0 -0
  66. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/ex12_resumable_chat.py +0 -0
  67. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/examples/langgraph/runner.py +0 -0
  68. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/_ast.py +0 -0
  69. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/_naming.py +0 -0
  70. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/_protocol.py +0 -0
  71. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/_proxy.py +0 -0
  72. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/_sync.py +0 -0
  73. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/aggregations.py +0 -0
  74. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/auth.py +0 -0
  75. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/client.py +0 -0
  76. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/client_sync.py +0 -0
  77. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/compiler.py +0 -0
  78. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/connection.py +0 -0
  79. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/derived.py +0 -0
  80. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/exceptions.py +0 -0
  81. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/functions.py +0 -0
  82. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/index.py +0 -0
  83. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/__init__.py +0 -0
  84. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langchain/__init__.py +0 -0
  85. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langchain/params.py +0 -0
  86. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langchain/retriever.py +0 -0
  87. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langchain/tool.py +0 -0
  88. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langchain/vector_store.py +0 -0
  89. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/__init__.py +0 -0
  90. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/_checkpoint_serde.py +0 -0
  91. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/_checkpointer_mixin.py +0 -0
  92. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/_memory_helpers.py +0 -0
  93. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/_memory_mixin.py +0 -0
  94. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/_utils.py +0 -0
  95. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/checkpointer.py +0 -0
  96. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/memory.py +0 -0
  97. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/nodes.py +0 -0
  98. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/router.py +0 -0
  99. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/integrations/langgraph/state.py +0 -0
  100. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/knowledge_graph.py +0 -0
  101. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/migrations/autodetector.py +0 -0
  102. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/migrations/operations.py +0 -0
  103. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/migrations/state.py +0 -0
  104. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/notifications.py +0 -0
  105. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/py.typed +0 -0
  106. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/relation.py +0 -0
  107. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/result.py +0 -0
  108. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/session.py +0 -0
  109. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/src/inputlayer/types.py +0 -0
  110. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/__init__.py +0 -0
  111. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/_mock_checkpoint_kg.py +0 -0
  112. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/_mock_memory_kg.py +0 -0
  113. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/conftest.py +0 -0
  114. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_aggregations.py +0 -0
  115. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_ast.py +0 -0
  116. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_autodetector.py +0 -0
  117. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_checkpointer_core.py +0 -0
  118. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_checkpointer_lifecycle.py +0 -0
  119. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_checkpointer_list.py +0 -0
  120. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_checkpointer_validation.py +0 -0
  121. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_compiler.py +0 -0
  122. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_connection.py +0 -0
  123. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_derived.py +0 -0
  124. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_escape_iql.py +0 -0
  125. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_executor.py +0 -0
  126. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_functions.py +0 -0
  127. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_integration.py +0 -0
  128. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_kg_node.py +0 -0
  129. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_kg_router.py +0 -0
  130. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_langchain.py +0 -0
  131. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_langgraph_integration.py +0 -0
  132. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_memory_core.py +0 -0
  133. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_memory_edge_cases.py +0 -0
  134. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_memory_helpers.py +0 -0
  135. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_naming.py +0 -0
  136. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_operations.py +0 -0
  137. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_protocol.py +0 -0
  138. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_proxy.py +0 -0
  139. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_relation.py +0 -0
  140. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_result.py +0 -0
  141. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_serde.py +0 -0
  142. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_state.py +0 -0
  143. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_sync.py +0 -0
  144. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/tests/test_types.py +0 -0
  145. {inputlayer_client_dev-0.1.0.dev915 → inputlayer_client_dev-0.1.0.dev919}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: inputlayer-client-dev
3
- Version: 0.1.0.dev915
3
+ Version: 0.1.0.dev919
4
4
  Summary: Python Object-Logic Mapper for InputLayer knowledge graph engine
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -51,7 +51,7 @@ pip install inputlayer-client-dev[all] # everything
51
51
 
52
52
  Requirements: Python 3.10+ and a running InputLayer server.
53
53
 
54
- This also installs the `il` CLI for managing schema migrations.
54
+ This also installs the `inputlayer-migrate` tool for schema migrations; with the InputLayer product CLI installed, use it as `il migration <verb>`.
55
55
 
56
56
  ## Quick Start
57
57
 
@@ -425,16 +425,16 @@ The SDK includes a Django-style migration system for production schema managemen
425
425
 
426
426
  ```bash
427
427
  # Generate a migration from your models
428
- il makemigrations --models myapp.models
428
+ il migration generate --models myapp.models
429
429
 
430
430
  # Apply pending migrations
431
- il migrate --url ws://localhost:8080/ws --kg production
431
+ il migration apply --url ws://localhost:8080/ws --kg production
432
432
 
433
433
  # Check status
434
- il showmigrations --url ws://localhost:8080/ws --kg production
434
+ il migration status --url ws://localhost:8080/ws --kg production
435
435
 
436
436
  # Rollback
437
- il revert --url ws://localhost:8080/ws --kg production 0001_initial
437
+ il migration revert --url ws://localhost:8080/ws --kg production 0001_initial
438
438
  ```
439
439
 
440
440
  The autodetector diffs your current Python models against the last migration's state and generates the minimal set of operations (create/drop relations, create/drop/replace rules, create/drop indexes). Each migration file is self-contained with a full state snapshot.
@@ -519,10 +519,10 @@ The autodetector diffs your current Python models against the last migration's s
519
519
 
520
520
  | Command | Description |
521
521
  |---------|-------------|
522
- | `il makemigrations --models <module>` | Generate migration from model diff |
523
- | `il migrate --url <ws> --kg <name>` | Apply pending migrations |
524
- | `il revert --url <ws> --kg <name> <target>` | Revert to a target migration |
525
- | `il showmigrations --url <ws> --kg <name>` | Show applied/pending status |
522
+ | `il migration generate --models <module>` | Generate migration from model diff |
523
+ | `il migration apply --url <ws> --kg <name>` | Apply pending migrations |
524
+ | `il migration revert --url <ws> --kg <name> <target>` | Revert to a target migration |
525
+ | `il migration status --url <ws> --kg <name>` | Show applied/pending status |
526
526
 
527
527
  ### Aggregation Functions
528
528
 
@@ -17,7 +17,7 @@ pip install inputlayer-client-dev[all] # everything
17
17
 
18
18
  Requirements: Python 3.10+ and a running InputLayer server.
19
19
 
20
- This also installs the `il` CLI for managing schema migrations.
20
+ This also installs the `inputlayer-migrate` tool for schema migrations; with the InputLayer product CLI installed, use it as `il migration <verb>`.
21
21
 
22
22
  ## Quick Start
23
23
 
@@ -391,16 +391,16 @@ The SDK includes a Django-style migration system for production schema managemen
391
391
 
392
392
  ```bash
393
393
  # Generate a migration from your models
394
- il makemigrations --models myapp.models
394
+ il migration generate --models myapp.models
395
395
 
396
396
  # Apply pending migrations
397
- il migrate --url ws://localhost:8080/ws --kg production
397
+ il migration apply --url ws://localhost:8080/ws --kg production
398
398
 
399
399
  # Check status
400
- il showmigrations --url ws://localhost:8080/ws --kg production
400
+ il migration status --url ws://localhost:8080/ws --kg production
401
401
 
402
402
  # Rollback
403
- il revert --url ws://localhost:8080/ws --kg production 0001_initial
403
+ il migration revert --url ws://localhost:8080/ws --kg production 0001_initial
404
404
  ```
405
405
 
406
406
  The autodetector diffs your current Python models against the last migration's state and generates the minimal set of operations (create/drop relations, create/drop/replace rules, create/drop indexes). Each migration file is self-contained with a full state snapshot.
@@ -485,10 +485,10 @@ The autodetector diffs your current Python models against the last migration's s
485
485
 
486
486
  | Command | Description |
487
487
  |---------|-------------|
488
- | `il makemigrations --models <module>` | Generate migration from model diff |
489
- | `il migrate --url <ws> --kg <name>` | Apply pending migrations |
490
- | `il revert --url <ws> --kg <name> <target>` | Revert to a target migration |
491
- | `il showmigrations --url <ws> --kg <name>` | Show applied/pending status |
488
+ | `il migration generate --models <module>` | Generate migration from model diff |
489
+ | `il migration apply --url <ws> --kg <name>` | Apply pending migrations |
490
+ | `il migration revert --url <ws> --kg <name> <target>` | Revert to a target migration |
491
+ | `il migration status --url <ws> --kg <name>` | Show applied/pending status |
492
492
 
493
493
  ### Aggregation Functions
494
494
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "inputlayer-client-dev"
7
- version = "0.1.0.dev915"
7
+ version = "0.1.0.dev919"
8
8
  description = "Python Object-Logic Mapper for InputLayer knowledge graph engine"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -34,7 +34,7 @@ dev = [
34
34
  ]
35
35
 
36
36
  [project.scripts]
37
- il = "inputlayer.migrations.cli:main"
37
+ inputlayer-migrate = "inputlayer.migrations.cli:main"
38
38
 
39
39
  [tool.hatch.build.targets.wheel]
40
40
  packages = ["src/inputlayer"]
@@ -68,8 +68,6 @@ from inputlayer.knowledge_graph import (
68
68
  WhyResult,
69
69
  )
70
70
 
71
- # Migrations
72
- from inputlayer.migrations import Migration
73
71
 
74
72
  # Notifications
75
73
  from inputlayer.notifications import NotificationEvent
@@ -116,8 +114,6 @@ __all__ = [
116
114
  "KnowledgeGraph",
117
115
  "KnowledgeGraphExistsError",
118
116
  "KnowledgeGraphNotFoundError",
119
- # Migrations
120
- "Migration",
121
117
  # Notifications
122
118
  "NotificationEvent",
123
119
  "PermissionError",
@@ -2,8 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
6
-
7
5
  from inputlayer.migrations.operations import (
8
6
  CreateIndex,
9
7
  CreateRelation,
@@ -19,22 +17,6 @@ from inputlayer.migrations.operations import (
19
17
  from inputlayer.migrations.state import ModelState
20
18
 
21
19
 
22
- class Migration:
23
- """Base class for migration files.
24
-
25
- Subclass as ``M`` in each migration file::
26
-
27
- class M(Migration):
28
- dependencies = ["0001_initial"]
29
- operations = [ops.CreateRelation(...)]
30
- state = {"relations": {...}, "rules": {...}, "indexes": {}}
31
- """
32
-
33
- dependencies: list[str] = [] # noqa: RUF012
34
- operations: list[Operation] = [] # noqa: RUF012
35
- state: dict[str, Any] = {} # noqa: RUF012
36
-
37
-
38
20
  __all__ = [
39
21
  "CreateIndex",
40
22
  "CreateRelation",
@@ -42,7 +24,6 @@ __all__ = [
42
24
  "DropIndex",
43
25
  "DropRelation",
44
26
  "DropRule",
45
- "Migration",
46
27
  "ModelState",
47
28
  "Operation",
48
29
  "ReplaceRule",
@@ -205,6 +205,20 @@ def _add_connection_args(parser: argparse.ArgumentParser) -> None:
205
205
  )
206
206
 
207
207
 
208
+ def _add_migrations_dir(sub: argparse.ArgumentParser) -> None:
209
+ """Accept --migrations-dir after the subcommand as well as before it.
210
+
211
+ Delegating callers (the `il` CLI) always place flags after the
212
+ subcommand. SUPPRESS keeps the subparser from clobbering the value the
213
+ global flag already parsed.
214
+ """
215
+ sub.add_argument(
216
+ "--migrations-dir",
217
+ default=argparse.SUPPRESS,
218
+ help="Directory for migration files (default: ./migrations)",
219
+ )
220
+
221
+
208
222
  def build_parser() -> argparse.ArgumentParser:
209
223
  """Build the CLI argument parser."""
210
224
  parser = argparse.ArgumentParser(
@@ -226,22 +240,26 @@ def build_parser() -> argparse.ArgumentParser:
226
240
  help="Python module path containing models (e.g. myapp.models)",
227
241
  )
228
242
  make.add_argument("--name", default=None, help="Custom migration name suffix")
243
+ _add_migrations_dir(make)
229
244
  make.set_defaults(func=_cmd_makemigrations)
230
245
 
231
246
  # migrate
232
247
  mig = subparsers.add_parser("migrate", help="Apply pending migrations")
233
248
  _add_connection_args(mig)
249
+ _add_migrations_dir(mig)
234
250
  mig.set_defaults(func=_cmd_migrate)
235
251
 
236
252
  # revert
237
253
  rev = subparsers.add_parser("revert", help="Revert migrations to a target")
238
254
  _add_connection_args(rev)
255
+ _add_migrations_dir(rev)
239
256
  rev.add_argument("target", help="Migration name to revert to (e.g. 0001_initial)")
240
257
  rev.set_defaults(func=_cmd_revert)
241
258
 
242
259
  # showmigrations
243
260
  show = subparsers.add_parser("showmigrations", help="Show migration status")
244
261
  _add_connection_args(show)
262
+ _add_migrations_dir(show)
245
263
  show.set_defaults(func=_cmd_showmigrations)
246
264
 
247
265
  return parser
@@ -249,6 +267,9 @@ def build_parser() -> argparse.ArgumentParser:
249
267
 
250
268
  def main(argv: list[str] | None = None) -> int:
251
269
  """CLI entry point."""
270
+ from inputlayer.exceptions import InputLayerError
271
+ from inputlayer.migrations.errors import MigrationError
272
+
252
273
  parser = build_parser()
253
274
  args = parser.parse_args(argv)
254
275
 
@@ -256,7 +277,11 @@ def main(argv: list[str] | None = None) -> int:
256
277
  parser.print_help()
257
278
  return 1
258
279
 
259
- return args.func(args)
280
+ try:
281
+ return args.func(args)
282
+ except (MigrationError, InputLayerError) as exc:
283
+ print(f"error: {exc}")
284
+ return 1
260
285
 
261
286
 
262
287
  if __name__ == "__main__":
@@ -0,0 +1,27 @@
1
+ """Migration errors and the engine-result check.
2
+
3
+ The WebSocket protocol reports engine failures as a result-set with a
4
+ single "error" column rather than an exception, so anything that must
5
+ know whether a statement actually executed has to check for that shape
6
+ explicitly. The migration stack must: silently swallowed errors are how
7
+ the recorder shipped broken (write-only applied-state) in the first
8
+ place.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+
16
+ class MigrationError(Exception):
17
+ """Raised when a migration fails to load, apply, or revert."""
18
+
19
+
20
+ def check_engine_result(result: Any, context: str) -> Any:
21
+ """Raise MigrationError if an execute() result is an engine error frame."""
22
+ columns = list(getattr(result, "columns", None) or [])
23
+ if columns == ["error"]:
24
+ rows = getattr(result, "rows", None) or []
25
+ detail = str(rows[0][0]) if rows and rows[0] else "unknown engine error"
26
+ raise MigrationError(f"engine error while {context}: {detail}")
27
+ return result
@@ -2,26 +2,35 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from inputlayer.migrations.errors import MigrationError, check_engine_result
5
6
  from inputlayer.migrations.loader import MigrationInfo
6
7
  from inputlayer.migrations.recorder import KGExecutor, MigrationRecorder
7
8
 
8
-
9
- class MigrationError(Exception):
10
- """Raised when a migration fails to apply or revert."""
9
+ __all__ = ["MigrationError", "apply_migration", "migrate", "revert_migration", "revert_to"]
11
10
 
12
11
 
13
12
  def apply_migration(kg: KGExecutor, migration: MigrationInfo) -> None:
14
- """Apply a single migration's forward operations."""
13
+ """Apply a single migration's forward operations.
14
+
15
+ Raises MigrationError on the first failing operation. Operations are
16
+ not transactional: earlier operations of this migration stay applied,
17
+ but the migration is NOT recorded as applied, so the failure is
18
+ visible and a re-run picks up where the schema actually is.
19
+ """
15
20
  for op in migration.operations:
16
21
  for cmd in op.forward_commands():
17
- kg.execute(cmd)
22
+ check_engine_result(
23
+ kg.execute(cmd), f"applying {migration.name} ({op.describe()})"
24
+ )
18
25
 
19
26
 
20
27
  def revert_migration(kg: KGExecutor, migration: MigrationInfo) -> None:
21
28
  """Revert a single migration's operations in reverse order."""
22
29
  for op in reversed(migration.operations):
23
30
  for cmd in op.backward_commands():
24
- kg.execute(cmd)
31
+ check_engine_result(
32
+ kg.execute(cmd), f"reverting {migration.name} ({op.describe()})"
33
+ )
25
34
 
26
35
 
27
36
  def migrate(
@@ -0,0 +1,135 @@
1
+ """Migration loader - discover and load JSON migration files from a directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from inputlayer.migrations.errors import MigrationError
11
+
12
+
13
+ @dataclass
14
+ class MigrationInfo:
15
+ """Metadata about a loaded migration file."""
16
+
17
+ name: str # e.g. "0001_initial"
18
+ number: int # e.g. 1
19
+ filename: str # e.g. "0001_initial.py"
20
+ dependencies: list[str]
21
+ operations: list # list[Operation]
22
+ state: dict[str, Any]
23
+
24
+ @property
25
+ def module_name(self) -> str:
26
+ return self.name
27
+
28
+
29
+ _MIGRATION_RE = re.compile(r"^(\d{4})_.+\.json$")
30
+
31
+
32
+ def _load_json_migration(entry: Path, name: str, number: int) -> MigrationInfo:
33
+ """Load a language-neutral JSON migration (the current format)."""
34
+ import json
35
+
36
+ from inputlayer.migrations.operations import operation_from_dict
37
+ from inputlayer.migrations.writer import MIGRATION_FORMAT
38
+
39
+ try:
40
+ document = json.loads(entry.read_text(encoding="utf-8"))
41
+ except json.JSONDecodeError as exc:
42
+ raise MigrationError(f"{entry.name}: invalid JSON: {exc}") from exc
43
+ if not isinstance(document, dict):
44
+ raise MigrationError(f"{entry.name}: migration document must be a JSON object")
45
+ fmt = document.get("format")
46
+ if not isinstance(fmt, int) or fmt > MIGRATION_FORMAT:
47
+ raise MigrationError(
48
+ f"{entry.name}: unsupported migration format {fmt!r} "
49
+ f"(this SDK reads up to {MIGRATION_FORMAT} - upgrade the SDK)"
50
+ )
51
+ state = dict(document.get("state", {}))
52
+ # JSON has no tuples; restore (col, type) pairs so state comparisons in
53
+ # the autodetector see no phantom changes against in-memory model state.
54
+ if "relations" in state:
55
+ state["relations"] = {
56
+ rel: [tuple(col) for col in cols] for rel, cols in state["relations"].items()
57
+ }
58
+ try:
59
+ operations = [operation_from_dict(d) for d in document.get("operations", [])]
60
+ except (KeyError, TypeError, ValueError) as exc:
61
+ raise MigrationError(f"{entry.name}: invalid operation: {exc}") from exc
62
+ return MigrationInfo(
63
+ name=name,
64
+ number=number,
65
+ filename=entry.name,
66
+ dependencies=list(document.get("dependencies", [])),
67
+ operations=operations,
68
+ state=state,
69
+ )
70
+
71
+
72
+ def load_migrations(directory: str | Path) -> list[MigrationInfo]:
73
+ """Discover and load all migration files from a directory.
74
+
75
+ Migrations are language-neutral JSON documents; that is the only
76
+ format (pre-1.0, no legacy loading). Non-matching files - including
77
+ stray .py files from older SDKs - are ignored. Returns migrations
78
+ sorted by number.
79
+ """
80
+ directory = Path(directory)
81
+ if not directory.is_dir():
82
+ return []
83
+
84
+ migrations: list[MigrationInfo] = []
85
+ for entry in sorted(directory.iterdir()):
86
+ if not entry.is_file():
87
+ continue
88
+ match = _MIGRATION_RE.match(entry.name)
89
+ if not match:
90
+ continue
91
+
92
+ number = int(match.group(1))
93
+ name = entry.stem # e.g. "0001_initial"
94
+
95
+ migrations.append(_load_json_migration(entry, name, number))
96
+
97
+ migrations.sort(key=lambda m: m.number)
98
+
99
+ # Duplicate names or numbers are always a mistake (a stale legacy .py
100
+ # twin of a regenerated .json, or a merge collision) and previously
101
+ # caused the same migration to be applied twice - destructively for
102
+ # Drop/Replace operations. Refuse loudly.
103
+ by_name: dict[str, str] = {}
104
+ by_number: dict[int, str] = {}
105
+ for m in migrations:
106
+ if m.name in by_name:
107
+ raise MigrationError(
108
+ f"duplicate migration name '{m.name}': {by_name[m.name]} and "
109
+ f"{m.filename} - delete the stale one"
110
+ )
111
+ if m.number in by_number:
112
+ raise MigrationError(
113
+ f"duplicate migration number {m.number:04d}: "
114
+ f"{by_number[m.number]} and {m.filename} - renumber one of them"
115
+ )
116
+ by_name[m.name] = m.filename
117
+ by_number[m.number] = m.filename
118
+
119
+ return migrations
120
+
121
+
122
+ def get_latest_state(directory: str | Path) -> dict[str, Any]:
123
+ """Get the state from the most recent migration, or empty if none exist."""
124
+ migrations = load_migrations(directory)
125
+ if not migrations:
126
+ return {"relations": {}, "rules": {}, "indexes": {}}
127
+ return migrations[-1].state
128
+
129
+
130
+ def get_next_number(directory: str | Path) -> int:
131
+ """Get the next migration number."""
132
+ migrations = load_migrations(directory)
133
+ if not migrations:
134
+ return 1
135
+ return migrations[-1].number + 1
@@ -0,0 +1,83 @@
1
+ """Migration recorder - track applied migrations in the DB."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Protocol
7
+
8
+ from inputlayer.migrations.errors import check_engine_result
9
+
10
+
11
+ class KGExecutor(Protocol):
12
+ """Minimal interface for executing IQL commands."""
13
+
14
+ def execute(self, iql: str) -> object: ...
15
+
16
+
17
+ # NOTE: must be a valid relation name (lowercase letter first). The previous
18
+ # dunder name was rejected by schema declarations but silently auto-created
19
+ # by inserts, leaving recorded state in a relation that queries could not
20
+ # read - migration status/revert never worked against a real engine.
21
+ MIGRATION_RELATION = "inputlayer_migrations"
22
+
23
+
24
+ class MigrationRecorder:
25
+ """Track which migrations have been applied using an internal relation."""
26
+
27
+ def __init__(self, kg: KGExecutor) -> None:
28
+ self._kg = kg
29
+
30
+ def _execute(self, iql: str, context: str) -> Any:
31
+ return check_engine_result(self._kg.execute(iql), context)
32
+
33
+ def ensure_schema(self) -> None:
34
+ """Create the migration tracking relation if it doesn't exist."""
35
+ # The engine reports "already registered" as an error frame; that is
36
+ # the one expected, harmless outcome, so only genuinely new failures
37
+ # surface. Anything else (bad name, auth, parse) must not be
38
+ # swallowed - that is how the recorder shipped write-only once.
39
+ result = self._kg.execute(
40
+ f"+{MIGRATION_RELATION}(name: string, applied_at: string)"
41
+ )
42
+ columns = list(getattr(result, "columns", None) or [])
43
+ if columns == ["error"]:
44
+ rows = getattr(result, "rows", None) or []
45
+ detail = str(rows[0][0]) if rows and rows[0] else ""
46
+ if "exist" not in detail and "registered" not in detail:
47
+ check_engine_result(result, "creating the migration tracking relation")
48
+
49
+ def get_applied(self) -> list[str]:
50
+ """Return sorted list of applied migration names.
51
+
52
+ Plain query form: the engine rejects `?X <- rel(...)` projections
53
+ ("Query cannot contain a rule definition"), which previously made
54
+ this return [] forever and broke status/revert/idempotent apply.
55
+ """
56
+ result = self._execute(
57
+ f"?{MIGRATION_RELATION}(Name, At)", "reading applied migrations"
58
+ )
59
+ rows = getattr(result, "rows", []) or []
60
+ return sorted({str(row[0]) for row in rows})
61
+
62
+ def record_applied(self, name: str) -> None:
63
+ """Record that a migration has been applied."""
64
+ now = datetime.now(timezone.utc).isoformat()
65
+ safe = _escape(name)
66
+ self._execute(
67
+ f'+{MIGRATION_RELATION}[("{safe}", "{now}")]',
68
+ f"recording {name} as applied",
69
+ )
70
+
71
+ def record_reverted(self, name: str) -> None:
72
+ """Remove the record for a reverted migration."""
73
+ safe = _escape(name)
74
+ self._execute(
75
+ f'-{MIGRATION_RELATION}(Name, At) <- '
76
+ f'{MIGRATION_RELATION}(Name, At), Name = "{safe}"',
77
+ f"recording {name} as reverted",
78
+ )
79
+
80
+
81
+ def _escape(value: str) -> str:
82
+ """Escape a string for interpolation into an IQL string literal."""
83
+ return value.replace("\\", "\\\\").replace('"', '\\"')
@@ -0,0 +1,51 @@
1
+ """Migration file writer - generates language-neutral JSON migration files.
2
+
3
+ A migration is data, not code: typed operations (each with enough
4
+ structure to derive both apply and revert IQL) plus a state snapshot.
5
+ Any SDK language - and the il CLI natively - can read and apply them.
6
+ JSON is the only migration format.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from typing import Any
14
+
15
+ from inputlayer.migrations.errors import MigrationError
16
+ from inputlayer.migrations.operations import Operation
17
+
18
+ MIGRATION_FORMAT = 1
19
+
20
+
21
+ def generate_migration(
22
+ number: int,
23
+ operations: list[Operation],
24
+ state: dict[str, Any],
25
+ dependencies: list[str],
26
+ *,
27
+ name_suffix: str | None = None,
28
+ ) -> tuple[str, str]:
29
+ """Generate a migration file.
30
+
31
+ Returns (filename, content).
32
+ """
33
+ if name_suffix is not None and not re.fullmatch(r"[a-z0-9_]+", name_suffix):
34
+ raise MigrationError(
35
+ f"invalid migration name {name_suffix!r}: use lowercase letters, "
36
+ "digits, and underscores only"
37
+ )
38
+ if number == 1 and name_suffix is None:
39
+ name_suffix = "initial"
40
+ elif name_suffix is None:
41
+ name_suffix = "auto"
42
+
43
+ filename = f"{number:04d}_{name_suffix}.json"
44
+
45
+ document = {
46
+ "format": MIGRATION_FORMAT,
47
+ "dependencies": dependencies,
48
+ "operations": [op.to_dict() for op in operations],
49
+ "state": state,
50
+ }
51
+ return filename, json.dumps(document, indent=2, sort_keys=False) + "\n"
@@ -18,6 +18,21 @@ class TestParser:
18
18
  def test_no_command_returns_1(self):
19
19
  assert main([]) == 1
20
20
 
21
+ def test_connection_error_prints_cleanly(self, capsys, monkeypatch):
22
+ # SDK errors (unreachable server, auth failure) must exit 1 with a
23
+ # clean message, not a traceback.
24
+ from inputlayer.exceptions import InputLayerConnectionError
25
+ from inputlayer.migrations import cli
26
+
27
+ def boom(args):
28
+ raise InputLayerConnectionError("Failed to connect to ws://nope")
29
+
30
+ monkeypatch.setattr(cli, "_cmd_showmigrations", boom)
31
+ rc = main(["showmigrations", "--url", "ws://nope", "--kg", "k"])
32
+ assert rc == 1
33
+ out = capsys.readouterr().out
34
+ assert "error: Failed to connect" in out
35
+
21
36
  def test_makemigrations_requires_models(self):
22
37
  parser = build_parser()
23
38
  with pytest.raises(SystemExit):
@@ -33,6 +48,33 @@ class TestParser:
33
48
  with pytest.raises(SystemExit):
34
49
  parser.parse_args(["revert", "--url", "ws://x", "--kg", "test"])
35
50
 
51
+ def test_migrations_dir_before_subcommand(self):
52
+ parser = build_parser()
53
+ args = parser.parse_args(
54
+ ["--migrations-dir", "custom", "makemigrations", "--models", "m"]
55
+ )
56
+ assert args.migrations_dir == "custom"
57
+
58
+ def test_migrations_dir_after_subcommand(self):
59
+ # Delegating callers (the il CLI) always place flags after the
60
+ # subcommand; this position must work on every subcommand.
61
+ parser = build_parser()
62
+ args = parser.parse_args(
63
+ ["makemigrations", "--models", "m", "--migrations-dir", "custom"]
64
+ )
65
+ assert args.migrations_dir == "custom"
66
+ args = parser.parse_args(
67
+ ["showmigrations", "--url", "ws://x", "--kg", "k",
68
+ "--migrations-dir", "custom"]
69
+ )
70
+ assert args.migrations_dir == "custom"
71
+
72
+ def test_migrations_dir_default_survives_subparser(self):
73
+ # SUPPRESS on the subparser copy must not clobber the global default.
74
+ parser = build_parser()
75
+ args = parser.parse_args(["makemigrations", "--models", "m"])
76
+ assert args.migrations_dir == "migrations"
77
+
36
78
  def test_showmigrations_parses(self):
37
79
  parser = build_parser()
38
80
  args = parser.parse_args(["showmigrations", "--url", "ws://x", "--kg", "test"])
@@ -125,7 +167,7 @@ class TestMakemigrations:
125
167
  "--models", "models_for_cli",
126
168
  ])
127
169
  assert result == 0
128
- files = list(migrations_dir.glob("0001_*.py"))
170
+ files = list(migrations_dir.glob("0001_*.json"))
129
171
  assert len(files) == 1
130
172
  assert "initial" in files[0].name
131
173
  finally:
@@ -168,7 +210,7 @@ class TestMakemigrations:
168
210
  ])
169
211
  assert result == 0
170
212
  # Should not create a second migration
171
- files = list(migrations_dir.glob("0002_*.py"))
213
+ files = list(migrations_dir.glob("0002_*.json"))
172
214
  assert len(files) == 0
173
215
  finally:
174
216
  sys.path.pop(0)
@@ -220,7 +262,7 @@ class TestMakemigrations:
220
262
  ])
221
263
  assert result == 0
222
264
 
223
- files = sorted(migrations_dir.glob("*.py"))
265
+ files = sorted(migrations_dir.glob("*.json"))
224
266
  assert len(files) == 2
225
267
 
226
268
  # Load and check dependency