tab-cli 0.1.5__tar.gz → 0.1.6__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 (116) hide show
  1. {tab_cli-0.1.5 → tab_cli-0.1.6}/AGENTS.md +1 -0
  2. {tab_cli-0.1.5 → tab_cli-0.1.6}/CHANGELOG.md +3 -0
  3. {tab_cli-0.1.5 → tab_cli-0.1.6}/PKG-INFO +1 -1
  4. tab_cli-0.1.6/docs/configuration.md +34 -0
  5. {tab_cli-0.1.5 → tab_cli-0.1.6}/mkdocs.yml +1 -0
  6. {tab_cli-0.1.5 → tab_cli-0.1.6}/pyproject.toml +1 -1
  7. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/cli.py +95 -27
  8. tab_cli-0.1.6/src/tab_cli/config.py +47 -0
  9. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/parquet.py +1 -1
  10. {tab_cli-0.1.5 → tab_cli-0.1.6}/tests/test_cli.py +164 -14
  11. tab_cli-0.1.5/src/tab_cli/config.py +0 -15
  12. {tab_cli-0.1.5 → tab_cli-0.1.6}/.gitignore +0 -0
  13. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/.gitignore +0 -0
  14. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +0 -0
  15. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/282ad8cf3324b2679a7d460c0fc324adfa21dcfad2f197ac6991b98ec91f98495bb3ddb2cba36ce5dfa28a52063a373bb03f5d2e34f6e0c7b6b81b3046a4d7d0 +0 -0
  16. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
  17. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/8926397375b8328137652c18cd4371808b214cb26864d77dd33c8eea895e10e62c297064c3d4c703d58ee11dee81bbf9851525e3a0c33151f54c164b8a4343b0 +0 -0
  18. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/89d29d938e64b1295e138faedfa5df6f6729a67e5ba8e0c3fdd1c9266e0f59a4bc35b8a21e14225072317879af3323fa0457e01d9b08608770bc23957feec6c0 +0 -0
  19. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/95fc2854c0fa2528b03e2bcabc7612f56f0bcb3d6f54a06293e21d1887885a449907b5b320a1a94bac712bf56fdad0a4184b17047c1fc179dfa83acab3f70d21 +0 -0
  20. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/9a58ceae46de4649375b6b880b8500c85d34c8e9bc650dc4c993afb2fa8d45c4180331821c6226a0fb1475b04a0b9a849502647ef9c4dcac74769eb501c885fa +0 -0
  21. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/a19ae062208aa20b7310705af3d26ef53095a9dabfad080883cc7a32e98687063179db95cb2c71ef9064801c59fa46d261f172d83e83f96a39c274387b59dca4 +0 -0
  22. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/d1169a8d10067493e42752c2a7615ff27f55bd90c38b91feef918958e29a2239ee289a0cde8385441d1f0fe9af1fb634fa6d56438b9f33ec382e81fa59d70b54 +0 -0
  23. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/e171e5c6039c050c2368584a537d36df221b1f9f23d285c0399b95b14608d67006229823e6831bff7d8b0c2f9e86ebaec9c6811461119195f430ade055073fed +0 -0
  24. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/ee118dd19aac4e1cb354f83a37dadca70bc5f086cc8d36cd0059b222bf8c7250824401ff681518d0120f8db1f96f0025c464dcfc3b1ee28777e8c76325760134 +0 -0
  25. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +0 -0
  26. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/282ad8cf3324b2679a7d460c0fc324adfa21dcfad2f197ac6991b98ec91f98495bb3ddb2cba36ce5dfa28a52063a373bb03f5d2e34f6e0c7b6b81b3046a4d7d0 +0 -0
  27. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
  28. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/8926397375b8328137652c18cd4371808b214cb26864d77dd33c8eea895e10e62c297064c3d4c703d58ee11dee81bbf9851525e3a0c33151f54c164b8a4343b0 +0 -0
  29. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/89d29d938e64b1295e138faedfa5df6f6729a67e5ba8e0c3fdd1c9266e0f59a4bc35b8a21e14225072317879af3323fa0457e01d9b08608770bc23957feec6c0 +0 -0
  30. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/95fc2854c0fa2528b03e2bcabc7612f56f0bcb3d6f54a06293e21d1887885a449907b5b320a1a94bac712bf56fdad0a4184b17047c1fc179dfa83acab3f70d21 +0 -0
  31. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/9a58ceae46de4649375b6b880b8500c85d34c8e9bc650dc4c993afb2fa8d45c4180331821c6226a0fb1475b04a0b9a849502647ef9c4dcac74769eb501c885fa +0 -0
  32. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/a19ae062208aa20b7310705af3d26ef53095a9dabfad080883cc7a32e98687063179db95cb2c71ef9064801c59fa46d261f172d83e83f96a39c274387b59dca4 +0 -0
  33. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/d1169a8d10067493e42752c2a7615ff27f55bd90c38b91feef918958e29a2239ee289a0cde8385441d1f0fe9af1fb634fa6d56438b9f33ec382e81fa59d70b54 +0 -0
  34. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/e171e5c6039c050c2368584a537d36df221b1f9f23d285c0399b95b14608d67006229823e6831bff7d8b0c2f9e86ebaec9c6811461119195f430ade055073fed +0 -0
  35. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/ee118dd19aac4e1cb354f83a37dadca70bc5f086cc8d36cd0059b222bf8c7250824401ff681518d0120f8db1f96f0025c464dcfc3b1ee28777e8c76325760134 +0 -0
  36. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/1029dd1c1f4430d8a667a0e48d0b817652c7ddca6f5ff56cac1755e5bb0c1cb7586935941c9b36f26cab05c0effe6154d073bd1dffbe37f22fed0a6e7d79201f +0 -0
  37. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/3228b9bef9d8374b5532b40df2da8be3bfc86de713bdad7fe620977ffa7c56db83928678caa792bf0d328db607028e045a9e41423ef7501e5b550651c3815ffe +0 -0
  38. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/424c9a53f5458b328d77ed6a943dc35662e949befa8725cfc7eead01a270417c8d07c1001b11623a088da8d2c9c34a41573314fa2394643147e4027e8a96a605 +0 -0
  39. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/44107e81054aa5544b36eab8d811908a559b4d9027dc3fa1762c44e39551652199ef2a31f9bbcda79773c906151c12578cb18c43e0045de8b20c357272e1c62a +0 -0
  40. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/52ea57bde33e8ef4718d833c2df3cf0a9e90fdcd5715c0caad50b4e37ea60aca2b8c71096de6920dc936c40b6291e896293f91c7cfd2aa96cf6c2aa49ef662c8 +0 -0
  41. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/5ea52f6d4771afda4747b9f44954102c02ae2d0686f8aa9eca36c29796bbba0d14e851c0dca0a6af17bfbbbac174e3be645ae708d958390168e85c64786fc9ef +0 -0
  42. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/86a1cfa113399acc7dd2dc90262a845558affb5e9373b6300dff68a485482c5e17ace9466bbc23b4301013ca1a27a577ca57ea838113f45bf321a64a242b1ad3 +0 -0
  43. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/8ef99a12ae0624db198d9ecd83014fb8353c4731e0ba9a472f1fc339784308e38f1287d0765a7b0444f1a89c218c76a820dc4c9a3a39c1cedcd7423a4f5f88dc +0 -0
  44. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/cb3e8da2bf2b7efae4a6e8fd0b8562dacd16ca0531b173a91480a9e60ee795ac5bec13fc6eb461e03edc9a26f5ff1d5ba53521a1c1a6c1ee1765b544b7d7bf73 +0 -0
  45. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/d27ad326963b75736b636adad9fb812eb3f2871e0efb4bc7db37d4b701a4282911eaaee91bed3a759e940769b667be1ed66f2d7f2f41ac3906b87ab7eec19c3a +0 -0
  46. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/type +0 -0
  47. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_heads/heads/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
  48. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_heads/type +0 -0
  49. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/282ad8cf3324b2679a7d460c0fc324adfa21dcfad2f197ac6991b98ec91f98495bb3ddb2cba36ce5dfa28a52063a373bb03f5d2e34f6e0c7b6b81b3046a4d7d0 +0 -0
  50. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
  51. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/8926397375b8328137652c18cd4371808b214cb26864d77dd33c8eea895e10e62c297064c3d4c703d58ee11dee81bbf9851525e3a0c33151f54c164b8a4343b0 +0 -0
  52. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/89d29d938e64b1295e138faedfa5df6f6729a67e5ba8e0c3fdd1c9266e0f59a4bc35b8a21e14225072317879af3323fa0457e01d9b08608770bc23957feec6c0 +0 -0
  53. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/95fc2854c0fa2528b03e2bcabc7612f56f0bcb3d6f54a06293e21d1887885a449907b5b320a1a94bac712bf56fdad0a4184b17047c1fc179dfa83acab3f70d21 +0 -0
  54. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/9a58ceae46de4649375b6b880b8500c85d34c8e9bc650dc4c993afb2fa8d45c4180331821c6226a0fb1475b04a0b9a849502647ef9c4dcac74769eb501c885fa +0 -0
  55. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/a19ae062208aa20b7310705af3d26ef53095a9dabfad080883cc7a32e98687063179db95cb2c71ef9064801c59fa46d261f172d83e83f96a39c274387b59dca4 +0 -0
  56. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/d1169a8d10067493e42752c2a7615ff27f55bd90c38b91feef918958e29a2239ee289a0cde8385441d1f0fe9af1fb634fa6d56438b9f33ec382e81fa59d70b54 +0 -0
  57. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/e171e5c6039c050c2368584a537d36df221b1f9f23d285c0399b95b14608d67006229823e6831bff7d8b0c2f9e86ebaec9c6811461119195f430ade055073fed +0 -0
  58. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/ee118dd19aac4e1cb354f83a37dadca70bc5f086cc8d36cd0059b222bf8c7250824401ff681518d0120f8db1f96f0025c464dcfc3b1ee28777e8c76325760134 +0 -0
  59. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/type +0 -0
  60. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/123b0e36150cf8e99d644d2dfd1a7b0c8d2f676a78248d6902516d9ae58903665c79ec3f5f6729b98a1237dcf2abc1d41e690ae0ad30888c84786bcc9de5e314 +0 -0
  61. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/2ac0b7d8b1fdfac82b3ff3926e0018f72ef2b48f85b41f5fa541271370e1197d41e37c2c6d62af0c6974658c4a9e5e945b8efcbcc7748bdd99bd9483f7e13e22 +0 -0
  62. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/67b8396b301935ff624ff98952c57d8ee021e7d885e1220053b993e7bc822a4cf061298e9643ded745c55f3ed8e923a6731524129993d2664f48b60660761145 +0 -0
  63. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/9c6fca696f77383cc87d068fe3f5912466b157dccc6465973e653dd4d2c02e2eca9c0725c071857bf3f4f0e263259eafc18f1739615a501e672bb2afd415316b +0 -0
  64. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/cae2e3e5952cb5ba93f27e3898d90dac6c41fc9e20c66ef0791e71b91dc103d924996c921c88179705264a224fb5d2bb29091fc8f18f0ea7a088aaabb859ea2a +0 -0
  65. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/cd8efb6da14127c81c37b56ead18a39b30f2cd154891185a6e906efb491dbf63f290eed3c31d4725f490a93208ffbe5cdc031d5b6de38fcb77cbb11f0357118f +0 -0
  66. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/e434359d6306a4f6997733b9b5308299984f05219c92e1b2a31f1203126be0fada5fc09a2ba86c98d1318981cb53997c7f8674ef25da6ca7bf9fd849598cd355 +0 -0
  67. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/ea75e7ccb42f52b013dc1b45bb4e6d692b37c5e38bd39356ba2806be83ca5ca03ce20481db2788428126f93663e6723f99f4d46c5c705f5b12b70e2127ab15ba +0 -0
  68. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/fa5dec7ee06fbc6cbb2798c8e98bab482a6750776de41406fb06893c83f71f5015ec7ee2873642714b7bbc1c496f880e3acf44ffdabf2f87a39e0fbd68a4cc46 +0 -0
  69. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/ffde172c6285c71851b22780e34962d8f9234067af59ff2dc38b74e905dc540fe35b944e8c0b2d4230dfcf778424a335494cd0aeea1bb4be2a11c4b5428ad465 +0 -0
  70. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/14e3f15273e204cadfe38dd2f38cebd1343a6bfbb91c6af0f5f9de6e9003d8ec8b0eb676975af9dca2d06ee5e6b3886c3c5d3755c6f149b6b09172cccc35adff +0 -0
  71. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/42e0f32dd10ba6ae9f2297ca8ad0bd16337f14545b29f956ce380a7ab92bab771cbc9e04755752a6fa13231286f724379d6662b0fb257ef2cc2c52fe680eb95d +0 -0
  72. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/466a441e56afdde383cef1d6127ca1d4c825157feb65a59f8f6ff5fa7a523bd683d0b7a6bb16c00afff53a890319a472d98da9af3dea5675168a4d424aa7af32 +0 -0
  73. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/482ae5a29fbe856c7272f2071b8b0f0359ee2d89ff392b8a900643fbd0836eccd067b8bf41909e206c90d45d6e7d8b6686b93ecaee5fe1a9060d87b672101310 +0 -0
  74. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/5403fa06049419ddcf620ce0dc20911583e1b1062b42630ee325aa5ac2f918dd266dfed93ebc808a2aecb37ac3a33e251fe1396fd24b8ed566bfbd61a81ff959 +0 -0
  75. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/56119fa0480cb66159978b1a8c9b031f9e978b7a3172eb87407a701fea34fdd90db7771cd433a2cf7e8a84a7b49c924f973eccb0eb05685ec42f36f7ef61cc06 +0 -0
  76. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/90b1f4de4ba65e0652f1de45e4c84b623f99bd9d9453667e19c7040857bd397e59e7a84406f2ab6204d25789d995c47c350d8abc47a4bbb102059f3111f20028 +0 -0
  77. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/9215bd4ac28fdecba111c63bace46d0f1c253ad3af44e0e74bd43d30757bef2e655538c1172c8f25769bce0a3c669b713440773c1859f0a136d9ca42501f2470 +0 -0
  78. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/a79320f784ef97b3d6297e55a48b17a517a38d95d5c61ba8d01c59d68dcd2ccf3a96479f4fc3c4cafdcd56dc7bd58b1cb987e079764c1646533ab32418900727 +0 -0
  79. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/abe6b7a350e1604cb8f5a2cd10cef13a019bd797770e6dc37414d33d98dcf36d8ec80a2ae13bd7dd2d2076772c0ffadec0b53ba72f4669b461fff2da5d30f1ba +0 -0
  80. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/b7689d54193a3798edd58d758966ad65ad57297c5276eab3e4ef07380779363efe9e462a149f4d42f55bbe004eb5ba88bf35df4c78ac975275530382390159d6 +0 -0
  81. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/e75f5f3431d172d7e9434dfaef2be50812105b3ead73eeb10345c9b6892e9cbb5ee0602ebb0ceaf5ab87d22f45930dc30d136d0aac77310fb0261b3857ffde9b +0 -0
  82. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/heads/56119fa0480cb66159978b1a8c9b031f9e978b7a3172eb87407a701fea34fdd90db7771cd433a2cf7e8a84a7b49c924f973eccb0eb05685ec42f36f7ef61cc06 +0 -0
  83. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/git_target +0 -0
  84. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/type +0 -0
  85. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/submodule_store/type +0 -0
  86. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/workspace_store/index +0 -0
  87. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/working_copy/checkout +0 -0
  88. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/working_copy/tree_state +0 -0
  89. {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/working_copy/type +0 -0
  90. {tab_cli-0.1.5 → tab_cli-0.1.6}/LICENSE +0 -0
  91. {tab_cli-0.1.5 → tab_cli-0.1.6}/Makefile +0 -0
  92. {tab_cli-0.1.5 → tab_cli-0.1.6}/README.md +0 -0
  93. {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/cli-ref.md +0 -0
  94. {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/cloud.md +0 -0
  95. {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/gen_assets.sh +0 -0
  96. {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/index.md +0 -0
  97. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/__init__.py +0 -0
  98. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/__init__.py +0 -0
  99. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/avro.py +0 -0
  100. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/base.py +0 -0
  101. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/csv.py +0 -0
  102. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/jsonl.py +0 -0
  103. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/handlers/__init__.py +0 -0
  104. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/handlers/base.py +0 -0
  105. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/handlers/cli_table.py +0 -0
  106. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/__init__.py +0 -0
  107. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/aws.py +0 -0
  108. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/az.py +0 -0
  109. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/base.py +0 -0
  110. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/fsspec.py +0 -0
  111. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/gcloud.py +0 -0
  112. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/local.py +0 -0
  113. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/style.py +0 -0
  114. {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/url_parser.py +0 -0
  115. {tab_cli-0.1.5 → tab_cli-0.1.6}/tests/__init__.py +0 -0
  116. {tab_cli-0.1.5 → tab_cli-0.1.6}/tests/assets/test.csv +0 -0
@@ -143,6 +143,7 @@ Use it as the default operating guide when changing code in this repo.
143
143
  ## Documentation
144
144
 
145
145
  - Update `docs/` and CLI help text when changing user-facing behavior.
146
+ - Update `CHANGELOG.md` with a clear description of user-facing changes and bug fixes.
146
147
  - Keep examples aligned with the actual command names and flags.
147
148
  - If you change build, test, or auth flows, reflect that in this file too.
148
149
 
@@ -1,3 +1,6 @@
1
+ - 0.1.6:
2
+ - Fixed bug in pyarrow loading of Parquet files.
3
+ - Added global config file support: settings can be persisted in `~/.config/tab/config.json`. Config file values serve as defaults that CLI flags override.
1
4
  - 0.1.5:
2
5
  - Added stdin support: use `-` as the file path to read from stdin (e.g. `cat data.csv | tab view -i csv -`). Requires `-i`/`--input-format` since format cannot be inferred. Works with `view`, `schema`, `summary`, `convert`, and `cat`.
3
6
  - Added row-wise JMESPath queries via `--jmespath` / `--jp` on `view`, `convert`, and `cat`. Object results become columns; non-object results go into a `value` column. `--sql` and `--jp` are mutually exclusive.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tab-cli
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: A CLI tool for tabular data
5
5
  Author-email: Tongfei Chen <tongfei@pm.me>
6
6
  License-File: LICENSE
@@ -0,0 +1,34 @@
1
+ # Configuration
2
+
3
+ `tab` supports a global configuration file at `~/.config/tab/config.json`. Settings in this file serve as defaults and are overridden by CLI flags.
4
+
5
+ ## Setup
6
+
7
+ Create the config file:
8
+
9
+ ```bash
10
+ mkdir -p ~/.config/tab
11
+ cat > ~/.config/tab/config.json << 'EOF'
12
+ {
13
+ "az_url_authority_is_account": false,
14
+ "sampling_size_for_schema_inference": 32
15
+ }
16
+ EOF
17
+ ```
18
+
19
+ ## Available settings
20
+
21
+ | Key | Type | Default | Description |
22
+ |-----|------|---------|-------------|
23
+ | `az_url_authority_is_account` | `bool` | `false` | Interpret `az://` URL authority as storage account name instead of container name. |
24
+ | `sampling_size_for_schema_inference` | `int` | `32` | Number of rows sampled for schema inference (e.g. when using `--jp`). |
25
+
26
+ ## Precedence
27
+
28
+ Settings are applied in this order (last wins):
29
+
30
+ 1. Built-in defaults
31
+ 2. Config file (`~/.config/tab/config.json`)
32
+ 3. CLI flags (e.g. `--az-url-authority-is-account`)
33
+
34
+ If the config file does not exist, built-in defaults are used. Unknown keys in the file are ignored with a warning.
@@ -31,6 +31,7 @@ repo_url: 'https://github.com/ctongfei/tab'
31
31
 
32
32
  nav:
33
33
  - 'Introduction': 'index.md'
34
+ - 'Configuration': 'configuration.md'
34
35
  - 'Cloud support': 'cloud.md'
35
36
  - 'CLI reference': 'cli-ref.md'
36
37
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tab-cli"
3
- version = "0.1.5"
3
+ version = "0.1.6"
4
4
  description = "A CLI tool for tabular data"
5
5
  authors = [{name = "Tongfei Chen", email = "tongfei@pm.me"}]
6
6
  readme = "README.md"
@@ -12,27 +12,67 @@ from rich.console import Console
12
12
  from rich.logging import RichHandler
13
13
 
14
14
  from tab_cli import config
15
- from tab_cli.config import Config
16
- from tab_cli.handlers import TableWriter, infer_reader, infer_writer, is_stdin, read_stdin
15
+ from tab_cli.config import Config, load_config_file
16
+ from tab_cli.handlers import (
17
+ TableWriter,
18
+ infer_reader,
19
+ infer_writer,
20
+ is_stdin,
21
+ read_stdin,
22
+ )
17
23
  from tab_cli.handlers.base import TableSchema, TableSummary
18
24
 
19
25
  # Reusable type aliases for common CLI options
20
- PathArg: TypeAlias = Annotated[str, typer.Argument(help="Path to the data file or directory")]
21
- PathsArg: TypeAlias = Annotated[list[str], typer.Argument(help="Paths to the data files or directories")]
22
- SrcArg: TypeAlias = Annotated[str, typer.Argument(help="Path to the source file or directory")]
23
- DstArg: TypeAlias = Annotated[str, typer.Argument(help="Path to the destination file or directory")]
24
- InputOpt: TypeAlias = Annotated[Optional[str], typer.Option("-i", "--input-format", help="Input format, auto-detected from extension if omitted")]
25
- OutputOpt: TypeAlias = Annotated[Optional[str], typer.Option("-o", "--output-format", help="Output format")]
26
- SqlOpt: TypeAlias = Annotated[Optional[str], typer.Option("--sql", help="SQL query to apply (table is available as 't')")]
26
+ PathArg: TypeAlias = Annotated[
27
+ str, typer.Argument(help="Path to the data file or directory")
28
+ ]
29
+ PathsArg: TypeAlias = Annotated[
30
+ list[str], typer.Argument(help="Paths to the data files or directories")
31
+ ]
32
+ SrcArg: TypeAlias = Annotated[
33
+ str, typer.Argument(help="Path to the source file or directory")
34
+ ]
35
+ DstArg: TypeAlias = Annotated[
36
+ str, typer.Argument(help="Path to the destination file or directory")
37
+ ]
38
+ InputOpt: TypeAlias = Annotated[
39
+ Optional[str],
40
+ typer.Option(
41
+ "-i",
42
+ "--input-format",
43
+ help="Input format, auto-detected from extension if omitted",
44
+ ),
45
+ ]
46
+ OutputOpt: TypeAlias = Annotated[
47
+ Optional[str], typer.Option("-o", "--output-format", help="Output format")
48
+ ]
49
+ SqlOpt: TypeAlias = Annotated[
50
+ Optional[str],
51
+ typer.Option("--sql", help="SQL query to apply (table is available as 't')"),
52
+ ]
27
53
  JmespathOpt: TypeAlias = Annotated[
28
54
  Optional[str],
29
- typer.Option("--jmespath", "--jp", help="JMESPath expression to apply to each row as JSON"),
55
+ typer.Option(
56
+ "--jmespath", "--jp", help="JMESPath expression to apply to each row as JSON"
57
+ ),
58
+ ]
59
+ LimitOpt: TypeAlias = Annotated[
60
+ Optional[int], typer.Option("--limit", help="Maximum number of rows to display")
61
+ ]
62
+ SkipOpt: TypeAlias = Annotated[
63
+ int, typer.Option("--skip", help="Number of rows to skip")
64
+ ]
65
+ MaxCellLenOpt: TypeAlias = Annotated[
66
+ Optional[int],
67
+ typer.Option("--max-cell-len", help="Truncate cell contents longer than this"),
68
+ ]
69
+ TableSvgOpt: TypeAlias = Annotated[
70
+ bool, typer.Option("--table-svg", help="Output table as SVG")
71
+ ]
72
+ NumPartitionsOpt: TypeAlias = Annotated[
73
+ Optional[int],
74
+ typer.Option("-n", "--num-partitions", help="Number of output partitions"),
30
75
  ]
31
- LimitOpt: TypeAlias = Annotated[Optional[int], typer.Option("--limit", help="Maximum number of rows to display")]
32
- SkipOpt: TypeAlias = Annotated[int, typer.Option("--skip", help="Number of rows to skip")]
33
- MaxCellLenOpt: TypeAlias = Annotated[Optional[int], typer.Option("--max-cell-len", help="Truncate cell contents longer than this")]
34
- TableSvgOpt: TypeAlias = Annotated[bool, typer.Option("--table-svg", help="Output table as SVG")]
35
- NumPartitionsOpt: TypeAlias = Annotated[Optional[int], typer.Option("-n", "--num-partitions", help="Number of output partitions")]
36
76
 
37
77
  app = typer.Typer(
38
78
  help="A CLI tool for viewing and manipulating tabular data.",
@@ -51,11 +91,12 @@ def main_callback(
51
91
  ] = False,
52
92
  log_level: Annotated[
53
93
  str,
54
- typer.Option("--log-level", help="Log level from {DEBUG, INFO, WARNING, ERROR, CRITICAL}"),
94
+ typer.Option(
95
+ "--log-level", help="Log level from {DEBUG, INFO, WARNING, ERROR, CRITICAL}"
96
+ ),
55
97
  ] = "INFO",
56
98
  ) -> None:
57
99
  """Global options for tab_cli CLI."""
58
- config.config.az_url_authority_is_account = az_url_authority_is_account
59
100
  logger.remove()
60
101
  logger.add(
61
102
  RichHandler(
@@ -66,6 +107,10 @@ def main_callback(
66
107
  format="{message}",
67
108
  level=log_level.upper(),
68
109
  )
110
+ load_config_file()
111
+ # CLI flags override config file values
112
+ if az_url_authority_is_account:
113
+ config.config.az_url_authority_is_account = az_url_authority_is_account
69
114
 
70
115
 
71
116
  def _apply_sql(lf: pl.LazyFrame, sql: str | None) -> pl.LazyFrame:
@@ -94,21 +139,29 @@ def _transform_jmespath_batch(
94
139
  if mode is None:
95
140
  mode = "object"
96
141
  elif mode != "object":
97
- raise ValueError("JMESPath query must return a consistent shape across rows")
142
+ raise ValueError(
143
+ "JMESPath query must return a consistent shape across rows"
144
+ )
98
145
 
99
146
  if expected_columns is not None:
100
147
  extra_columns = set(result) - set(expected_columns)
101
148
  if extra_columns:
102
149
  extras = ", ".join(sorted(extra_columns))
103
- raise ValueError(f"JMESPath query produced unexpected columns: {extras}")
104
- normalized_row = {column: result.get(column) for column in expected_columns}
150
+ raise ValueError(
151
+ f"JMESPath query produced unexpected columns: {extras}"
152
+ )
153
+ normalized_row = {
154
+ column: result.get(column) for column in expected_columns
155
+ }
105
156
  else:
106
157
  normalized_row = result
107
158
  else:
108
159
  if mode is None:
109
160
  mode = "value"
110
161
  elif mode != "value":
111
- raise ValueError("JMESPath query must return a consistent shape across rows")
162
+ raise ValueError(
163
+ "JMESPath query must return a consistent shape across rows"
164
+ )
112
165
  normalized_row = {"value": result}
113
166
 
114
167
  rows.append(normalized_row)
@@ -143,10 +196,14 @@ def _apply_jmespath(lf: pl.LazyFrame, expression: str) -> pl.LazyFrame:
143
196
  )
144
197
 
145
198
 
146
- def _apply_query(lf: pl.LazyFrame, sql: str | None, jmespath_expr: str | None) -> pl.LazyFrame:
199
+ def _apply_query(
200
+ lf: pl.LazyFrame, sql: str | None, jmespath_expr: str | None
201
+ ) -> pl.LazyFrame:
147
202
  """Apply exactly zero or one supported query transform to a LazyFrame."""
148
203
  if sql is not None and jmespath_expr is not None:
149
- raise ValueError("At most one query may be provided: use either --sql or --jmespath/--jp")
204
+ raise ValueError(
205
+ "At most one query may be provided: use either --sql or --jmespath/--jp"
206
+ )
150
207
  if sql is not None:
151
208
  return _apply_sql(lf, sql)
152
209
  if jmespath_expr is not None:
@@ -196,11 +253,18 @@ def view(
196
253
  reader = infer_reader(path, format=input)
197
254
  lf = reader.read(path)
198
255
  lf = _apply_query(lf, sql=sql, jmespath_expr=jmespath_expr)
199
- lf, truncated = _apply_limit(lf, limit=limit, skip=skip, default_limit=20 if limit is None else None)
200
- writer = infer_writer("table-svg" if table_svg else None, truncated=truncated, max_cell_len=max_cell_len)
256
+ lf, truncated = _apply_limit(
257
+ lf, limit=limit, skip=skip, default_limit=20 if limit is None else None
258
+ )
259
+ writer = infer_writer(
260
+ "table-svg" if table_svg else None,
261
+ truncated=truncated,
262
+ max_cell_len=max_cell_len,
263
+ )
201
264
  for chunk in writer.write(lf):
202
265
  sys.stdout.buffer.write(chunk)
203
266
 
267
+
204
268
  @app.command()
205
269
  def schema(
206
270
  path: PathArg,
@@ -258,7 +322,9 @@ def convert(
258
322
  elif input is not None:
259
323
  writer = infer_writer(format=input)
260
324
  else:
261
- raise ValueError("Output format (-o/--output-format) is required when reading from stdin (-)")
325
+ raise ValueError(
326
+ "Output format (-o/--output-format) is required when reading from stdin (-)"
327
+ )
262
328
  assert isinstance(writer, TableWriter)
263
329
  writer.write_to_path(lf, dst, partitions=num_partitions)
264
330
  else:
@@ -305,7 +371,9 @@ def cat(
305
371
  writer = infer_writer(format=input)
306
372
  assert isinstance(writer, TableWriter)
307
373
  else:
308
- raise ValueError("Output format (-o/--output-format) or input format (-i/--input-format) is required when reading from stdin (-)")
374
+ raise ValueError(
375
+ "Output format (-o/--output-format) or input format (-i/--input-format) is required when reading from stdin (-)"
376
+ )
309
377
  for chunk in writer.write(lf):
310
378
  sys.stdout.buffer.write(chunk)
311
379
 
@@ -0,0 +1,47 @@
1
+ """Global configuration for tab_cli-cli."""
2
+
3
+ import json
4
+ from dataclasses import dataclass, fields
5
+ from pathlib import Path
6
+
7
+ from loguru import logger
8
+
9
+ CONFIG_DIR = Path.home() / ".config" / "tab"
10
+ CONFIG_FILE = CONFIG_DIR / "config.json"
11
+
12
+
13
+ @dataclass
14
+ class Config:
15
+ """Global configuration settings."""
16
+
17
+ az_url_authority_is_account: bool = False
18
+ sampling_size_for_schema_inference: int = 32
19
+
20
+
21
+ # Global config instance
22
+ config: Config = Config()
23
+
24
+
25
+ def load_config_file(path: Path = CONFIG_FILE) -> None:
26
+ """Load settings from a JSON config file into the global config.
27
+
28
+ Unknown keys are logged and ignored. Type mismatches raise ValueError.
29
+ If the file does not exist, this is a no-op.
30
+ """
31
+ if not path.is_file():
32
+ return
33
+
34
+ text = path.read_text(encoding="utf-8")
35
+ data = json.loads(text)
36
+ if not isinstance(data, dict):
37
+ raise ValueError(
38
+ f"Config file must contain a JSON object, got {type(data).__name__}"
39
+ )
40
+
41
+ known = {f.name: f.type for f in fields(Config)}
42
+ for key, value in data.items():
43
+ if key not in known:
44
+ logger.warning("Unknown config key '{}' in {}", key, path)
45
+ continue
46
+ setattr(config, key, value)
47
+ logger.debug("Loaded config from {}", path)
@@ -30,7 +30,7 @@ def _scan_parquet_with_pyarrow_fallback(
30
30
  "Polars native Parquet reader failed ({}), retrying with PyArrow backend",
31
31
  e,
32
32
  )
33
- return pl.scan_parquet(url, storage_options=storage_options, use_pyarrow=True)
33
+ return pl.read_parquet(url, storage_options=storage_options, use_pyarrow=True).lazy()
34
34
 
35
35
 
36
36
  class ParquetFormat(FormatHandler):
@@ -2,10 +2,13 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ from unittest.mock import patch
5
6
 
6
7
  from typer.testing import CliRunner
7
8
 
9
+ from tab_cli import config as config_module
8
10
  from tab_cli.cli import app
11
+ from tab_cli.config import Config, load_config_file
9
12
 
10
13
  runner = CliRunner()
11
14
  TEST_CSV = os.path.join(os.path.dirname(__file__), "assets", "test.csv")
@@ -57,7 +60,11 @@ class TestView:
57
60
  result = runner.invoke(app, ["view", TEST_CSV])
58
61
  assert result.exit_code == 0
59
62
  # 8 rows < 20 default limit, so no truncation
60
- lines_with_ellipsis = [line for line in result.output.splitlines() if line.strip() == "... ... ... ... ... ..."]
63
+ lines_with_ellipsis = [
64
+ line
65
+ for line in result.output.splitlines()
66
+ if line.strip() == "... ... ... ... ... ..."
67
+ ]
61
68
  assert len(lines_with_ellipsis) == 0
62
69
 
63
70
 
@@ -66,7 +73,11 @@ class TestCat:
66
73
  result = runner.invoke(app, ["cat", TEST_CSV])
67
74
  assert result.exit_code == 0
68
75
  # Should output in CSV format (the input format), not a Rich table
69
- assert "Participant_ID," in result.output or "Participant_ID\t" in result.output or "P001" in result.output
76
+ assert (
77
+ "Participant_ID," in result.output
78
+ or "Participant_ID\t" in result.output
79
+ or "P001" in result.output
80
+ )
70
81
 
71
82
  def test_output_format_csv(self):
72
83
  result = runner.invoke(app, ["cat", TEST_CSV, "-o", "csv"])
@@ -93,21 +104,36 @@ class TestCat:
93
104
 
94
105
  class TestSqlOption:
95
106
  def test_view_with_sql(self):
96
- result = runner.invoke(app, ["view", TEST_CSV, "--sql", "SELECT * FROM t WHERE Status = 'Baseline'"])
107
+ result = runner.invoke(
108
+ app,
109
+ ["view", TEST_CSV, "--sql", "SELECT * FROM t WHERE Status = 'Baseline'"],
110
+ )
97
111
  assert result.exit_code == 0
98
112
  assert "Baseline" in result.output
99
113
  # Should show as a table by default
100
114
  assert "Active" not in result.output
101
115
 
102
116
  def test_view_with_sql_and_limit(self):
103
- result = runner.invoke(app, ["view", TEST_CSV, "--sql", "SELECT * FROM t", "--limit", "2"])
117
+ result = runner.invoke(
118
+ app, ["view", TEST_CSV, "--sql", "SELECT * FROM t", "--limit", "2"]
119
+ )
104
120
  assert result.exit_code == 0
105
121
  # Should have limited rows
106
122
  count = sum(1 for line in result.output.splitlines() if "P00" in line)
107
123
  assert count <= 2
108
124
 
109
125
  def test_cat_with_sql_and_output_format(self):
110
- result = runner.invoke(app, ["cat", TEST_CSV, "--sql", "SELECT Participant_ID, Status FROM t", "-o", "csv"])
126
+ result = runner.invoke(
127
+ app,
128
+ [
129
+ "cat",
130
+ TEST_CSV,
131
+ "--sql",
132
+ "SELECT Participant_ID, Status FROM t",
133
+ "-o",
134
+ "csv",
135
+ ],
136
+ )
111
137
  assert result.exit_code == 0
112
138
  lines = result.output.strip().splitlines()
113
139
  assert "Participant_ID" in lines[0]
@@ -116,38 +142,68 @@ class TestSqlOption:
116
142
 
117
143
  class TestJmespathOption:
118
144
  def test_view_with_jmespath_object(self):
119
- result = runner.invoke(app, ["view", TEST_CSV, "--jp", "{id: Participant_ID, status: Status}"])
145
+ result = runner.invoke(
146
+ app, ["view", TEST_CSV, "--jp", "{id: Participant_ID, status: Status}"]
147
+ )
120
148
  assert result.exit_code == 0
121
149
  assert "id" in result.output
122
150
  assert "status" in result.output
123
151
  assert "Baseline" in result.output
124
152
 
125
153
  def test_cat_with_jmespath_object_output(self):
126
- result = runner.invoke(app, ["cat", TEST_CSV, "--jp", "{id: Participant_ID, status: Status}", "-o", "jsonl"])
154
+ result = runner.invoke(
155
+ app,
156
+ [
157
+ "cat",
158
+ TEST_CSV,
159
+ "--jp",
160
+ "{id: Participant_ID, status: Status}",
161
+ "-o",
162
+ "jsonl",
163
+ ],
164
+ )
127
165
  assert result.exit_code == 0
128
166
  first_row = json.loads(result.output.strip().splitlines()[0])
129
167
  assert first_row == {"id": "P001", "status": "Baseline"}
130
168
 
131
169
  def test_cat_with_jmespath_scalar_output(self):
132
- result = runner.invoke(app, ["cat", TEST_CSV, "--jp", "Participant_ID", "-o", "jsonl"])
170
+ result = runner.invoke(
171
+ app, ["cat", TEST_CSV, "--jp", "Participant_ID", "-o", "jsonl"]
172
+ )
133
173
  assert result.exit_code == 0
134
174
  first_row = json.loads(result.output.strip().splitlines()[0])
135
175
  assert first_row == {"value": "P001"}
136
176
 
137
177
  def test_cat_with_jmespath_list_output(self):
138
- result = runner.invoke(app, ["cat", TEST_CSV, "--jp", "[Participant_ID, Status]", "-o", "jsonl"])
178
+ result = runner.invoke(
179
+ app, ["cat", TEST_CSV, "--jp", "[Participant_ID, Status]", "-o", "jsonl"]
180
+ )
139
181
  assert result.exit_code == 0
140
182
  first_row = json.loads(result.output.strip().splitlines()[0])
141
183
  assert first_row == {"value": ["P001", "Baseline"]}
142
184
 
143
185
  def test_cat_with_jmespath_null_output(self):
144
- result = runner.invoke(app, ["cat", TEST_CSV, "--jp", "MissingField", "-o", "jsonl"])
186
+ result = runner.invoke(
187
+ app, ["cat", TEST_CSV, "--jp", "MissingField", "-o", "jsonl"]
188
+ )
145
189
  assert result.exit_code == 0
146
190
  first_row = json.loads(result.output.strip().splitlines()[0])
147
191
  assert first_row == {"value": None}
148
192
 
149
193
  def test_sql_and_jmespath_are_mutually_exclusive(self):
150
- result = runner.invoke(app, ["cat", TEST_CSV, "--sql", "SELECT * FROM t", "--jp", "Participant_ID", "-o", "jsonl"])
194
+ result = runner.invoke(
195
+ app,
196
+ [
197
+ "cat",
198
+ TEST_CSV,
199
+ "--sql",
200
+ "SELECT * FROM t",
201
+ "--jp",
202
+ "Participant_ID",
203
+ "-o",
204
+ "jsonl",
205
+ ],
206
+ )
151
207
  assert result.exit_code != 0
152
208
  assert result.exception is not None
153
209
  assert "At most one query may be provided" in str(result.exception)
@@ -174,7 +230,9 @@ class TestStdin:
174
230
  assert len(lines) == 9 # header + 8 data rows
175
231
 
176
232
  def test_cat_stdin_with_output_format(self):
177
- result = runner.invoke(app, ["cat", "-i", "csv", "-o", "tsv", "-"], input=TEST_CSV_TEXT)
233
+ result = runner.invoke(
234
+ app, ["cat", "-i", "csv", "-o", "tsv", "-"], input=TEST_CSV_TEXT
235
+ )
178
236
  assert result.exit_code == 0
179
237
  lines = result.output.strip().splitlines()
180
238
  assert "\t" in lines[0]
@@ -193,7 +251,14 @@ class TestStdin:
193
251
  def test_view_stdin_with_sql(self):
194
252
  result = runner.invoke(
195
253
  app,
196
- ["view", "-i", "csv", "--sql", "SELECT * FROM t WHERE Status = 'Baseline'", "-"],
254
+ [
255
+ "view",
256
+ "-i",
257
+ "csv",
258
+ "--sql",
259
+ "SELECT * FROM t WHERE Status = 'Baseline'",
260
+ "-",
261
+ ],
197
262
  input=TEST_CSV_TEXT,
198
263
  )
199
264
  assert result.exit_code == 0
@@ -201,7 +266,92 @@ class TestStdin:
201
266
  assert "Active" not in result.output
202
267
 
203
268
  def test_view_stdin_with_limit(self):
204
- result = runner.invoke(app, ["view", "-i", "csv", "--limit", "2", "-"], input=TEST_CSV_TEXT)
269
+ result = runner.invoke(
270
+ app, ["view", "-i", "csv", "--limit", "2", "-"], input=TEST_CSV_TEXT
271
+ )
205
272
  assert result.exit_code == 0
206
273
  assert "P001" in result.output
207
274
  assert "P003" not in result.output
275
+
276
+
277
+ class TestConfigFile:
278
+ """Tests for loading config from ~/.config/tab/config.json."""
279
+
280
+ def setup_method(self):
281
+ """Reset global config before each test."""
282
+ config_module.config = Config()
283
+
284
+ def test_load_missing_file(self, tmp_path):
285
+ """No-op when the config file does not exist."""
286
+ load_config_file(tmp_path / "nonexistent.json")
287
+ assert config_module.config.az_url_authority_is_account is False
288
+ assert config_module.config.sampling_size_for_schema_inference == 32
289
+
290
+ def test_load_valid_config(self, tmp_path):
291
+ """Known keys are applied to the global config."""
292
+ cfg = tmp_path / "config.json"
293
+ cfg.write_text(
294
+ json.dumps(
295
+ {
296
+ "az_url_authority_is_account": True,
297
+ "sampling_size_for_schema_inference": 64,
298
+ }
299
+ )
300
+ )
301
+ load_config_file(cfg)
302
+ assert config_module.config.az_url_authority_is_account is True
303
+ assert config_module.config.sampling_size_for_schema_inference == 64
304
+
305
+ def test_load_partial_config(self, tmp_path):
306
+ """Only specified keys are changed; others keep defaults."""
307
+ cfg = tmp_path / "config.json"
308
+ cfg.write_text(json.dumps({"sampling_size_for_schema_inference": 128}))
309
+ load_config_file(cfg)
310
+ assert config_module.config.az_url_authority_is_account is False
311
+ assert config_module.config.sampling_size_for_schema_inference == 128
312
+
313
+ def test_unknown_keys_ignored(self, tmp_path):
314
+ """Unknown keys are silently ignored (with a warning log)."""
315
+ cfg = tmp_path / "config.json"
316
+ cfg.write_text(
317
+ json.dumps(
318
+ {"unknown_key": "value", "sampling_size_for_schema_inference": 16}
319
+ )
320
+ )
321
+ load_config_file(cfg)
322
+ assert config_module.config.sampling_size_for_schema_inference == 16
323
+ assert not hasattr(config_module.config, "unknown_key")
324
+
325
+ def test_invalid_json_raises(self, tmp_path):
326
+ """Non-object JSON raises ValueError."""
327
+ cfg = tmp_path / "config.json"
328
+ cfg.write_text('"just a string"')
329
+ try:
330
+ load_config_file(cfg)
331
+ assert False, "Expected ValueError"
332
+ except ValueError as e:
333
+ assert "JSON object" in str(e)
334
+
335
+ def test_cli_flag_overrides_config_file(self, tmp_path):
336
+ """CLI --az-url-authority-is-account overrides the config file value."""
337
+ cfg = tmp_path / "config.json"
338
+ cfg.write_text(json.dumps({"az_url_authority_is_account": False}))
339
+ with patch(
340
+ "tab_cli.cli.load_config_file", side_effect=lambda: load_config_file(cfg)
341
+ ):
342
+ result = runner.invoke(
343
+ app, ["--az-url-authority-is-account", "view", TEST_CSV]
344
+ )
345
+ assert result.exit_code == 0
346
+ assert config_module.config.az_url_authority_is_account is True
347
+
348
+ def test_config_file_sets_default(self, tmp_path):
349
+ """Config file value is used when CLI flag is not passed."""
350
+ cfg = tmp_path / "config.json"
351
+ cfg.write_text(json.dumps({"az_url_authority_is_account": True}))
352
+ with patch(
353
+ "tab_cli.cli.load_config_file", side_effect=lambda: load_config_file(cfg)
354
+ ):
355
+ result = runner.invoke(app, ["view", TEST_CSV])
356
+ assert result.exit_code == 0
357
+ assert config_module.config.az_url_authority_is_account is True
@@ -1,15 +0,0 @@
1
- """Global configuration for tab_cli-cli."""
2
-
3
- from dataclasses import dataclass
4
-
5
-
6
- @dataclass
7
- class Config:
8
- """Global configuration settings."""
9
-
10
- az_url_authority_is_account: bool = False
11
- sampling_size_for_schema_inference: int = 32
12
-
13
-
14
- # Global config instance
15
- config: Config = Config()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes