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.
- {tab_cli-0.1.5 → tab_cli-0.1.6}/AGENTS.md +1 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/CHANGELOG.md +3 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/PKG-INFO +1 -1
- tab_cli-0.1.6/docs/configuration.md +34 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/mkdocs.yml +1 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/pyproject.toml +1 -1
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/cli.py +95 -27
- tab_cli-0.1.6/src/tab_cli/config.py +47 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/parquet.py +1 -1
- {tab_cli-0.1.5 → tab_cli-0.1.6}/tests/test_cli.py +164 -14
- tab_cli-0.1.5/src/tab_cli/config.py +0 -15
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.gitignore +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/.gitignore +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/282ad8cf3324b2679a7d460c0fc324adfa21dcfad2f197ac6991b98ec91f98495bb3ddb2cba36ce5dfa28a52063a373bb03f5d2e34f6e0c7b6b81b3046a4d7d0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/8926397375b8328137652c18cd4371808b214cb26864d77dd33c8eea895e10e62c297064c3d4c703d58ee11dee81bbf9851525e3a0c33151f54c164b8a4343b0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/89d29d938e64b1295e138faedfa5df6f6729a67e5ba8e0c3fdd1c9266e0f59a4bc35b8a21e14225072317879af3323fa0457e01d9b08608770bc23957feec6c0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/95fc2854c0fa2528b03e2bcabc7612f56f0bcb3d6f54a06293e21d1887885a449907b5b320a1a94bac712bf56fdad0a4184b17047c1fc179dfa83acab3f70d21 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/9a58ceae46de4649375b6b880b8500c85d34c8e9bc650dc4c993afb2fa8d45c4180331821c6226a0fb1475b04a0b9a849502647ef9c4dcac74769eb501c885fa +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/a19ae062208aa20b7310705af3d26ef53095a9dabfad080883cc7a32e98687063179db95cb2c71ef9064801c59fa46d261f172d83e83f96a39c274387b59dca4 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/d1169a8d10067493e42752c2a7615ff27f55bd90c38b91feef918958e29a2239ee289a0cde8385441d1f0fe9af1fb634fa6d56438b9f33ec382e81fa59d70b54 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/e171e5c6039c050c2368584a537d36df221b1f9f23d285c0399b95b14608d67006229823e6831bff7d8b0c2f9e86ebaec9c6811461119195f430ade055073fed +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/op_links/ee118dd19aac4e1cb354f83a37dadca70bc5f086cc8d36cd0059b222bf8c7250824401ff681518d0120f8db1f96f0025c464dcfc3b1ee28777e8c76325760134 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/282ad8cf3324b2679a7d460c0fc324adfa21dcfad2f197ac6991b98ec91f98495bb3ddb2cba36ce5dfa28a52063a373bb03f5d2e34f6e0c7b6b81b3046a4d7d0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/8926397375b8328137652c18cd4371808b214cb26864d77dd33c8eea895e10e62c297064c3d4c703d58ee11dee81bbf9851525e3a0c33151f54c164b8a4343b0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/89d29d938e64b1295e138faedfa5df6f6729a67e5ba8e0c3fdd1c9266e0f59a4bc35b8a21e14225072317879af3323fa0457e01d9b08608770bc23957feec6c0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/95fc2854c0fa2528b03e2bcabc7612f56f0bcb3d6f54a06293e21d1887885a449907b5b320a1a94bac712bf56fdad0a4184b17047c1fc179dfa83acab3f70d21 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/9a58ceae46de4649375b6b880b8500c85d34c8e9bc650dc4c993afb2fa8d45c4180331821c6226a0fb1475b04a0b9a849502647ef9c4dcac74769eb501c885fa +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/a19ae062208aa20b7310705af3d26ef53095a9dabfad080883cc7a32e98687063179db95cb2c71ef9064801c59fa46d261f172d83e83f96a39c274387b59dca4 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/d1169a8d10067493e42752c2a7615ff27f55bd90c38b91feef918958e29a2239ee289a0cde8385441d1f0fe9af1fb634fa6d56438b9f33ec382e81fa59d70b54 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/e171e5c6039c050c2368584a537d36df221b1f9f23d285c0399b95b14608d67006229823e6831bff7d8b0c2f9e86ebaec9c6811461119195f430ade055073fed +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/operations/ee118dd19aac4e1cb354f83a37dadca70bc5f086cc8d36cd0059b222bf8c7250824401ff681518d0120f8db1f96f0025c464dcfc3b1ee28777e8c76325760134 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/1029dd1c1f4430d8a667a0e48d0b817652c7ddca6f5ff56cac1755e5bb0c1cb7586935941c9b36f26cab05c0effe6154d073bd1dffbe37f22fed0a6e7d79201f +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/3228b9bef9d8374b5532b40df2da8be3bfc86de713bdad7fe620977ffa7c56db83928678caa792bf0d328db607028e045a9e41423ef7501e5b550651c3815ffe +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/424c9a53f5458b328d77ed6a943dc35662e949befa8725cfc7eead01a270417c8d07c1001b11623a088da8d2c9c34a41573314fa2394643147e4027e8a96a605 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/44107e81054aa5544b36eab8d811908a559b4d9027dc3fa1762c44e39551652199ef2a31f9bbcda79773c906151c12578cb18c43e0045de8b20c357272e1c62a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/52ea57bde33e8ef4718d833c2df3cf0a9e90fdcd5715c0caad50b4e37ea60aca2b8c71096de6920dc936c40b6291e896293f91c7cfd2aa96cf6c2aa49ef662c8 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/5ea52f6d4771afda4747b9f44954102c02ae2d0686f8aa9eca36c29796bbba0d14e851c0dca0a6af17bfbbbac174e3be645ae708d958390168e85c64786fc9ef +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/86a1cfa113399acc7dd2dc90262a845558affb5e9373b6300dff68a485482c5e17ace9466bbc23b4301013ca1a27a577ca57ea838113f45bf321a64a242b1ad3 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/8ef99a12ae0624db198d9ecd83014fb8353c4731e0ba9a472f1fc339784308e38f1287d0765a7b0444f1a89c218c76a820dc4c9a3a39c1cedcd7423a4f5f88dc +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/cb3e8da2bf2b7efae4a6e8fd0b8562dacd16ca0531b173a91480a9e60ee795ac5bec13fc6eb461e03edc9a26f5ff1d5ba53521a1c1a6c1ee1765b544b7d7bf73 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/segments/d27ad326963b75736b636adad9fb812eb3f2871e0efb4bc7db37d4b701a4282911eaaee91bed3a759e940769b667be1ed66f2d7f2f41ac3906b87ab7eec19c3a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/index/type +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_heads/heads/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_heads/type +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/282ad8cf3324b2679a7d460c0fc324adfa21dcfad2f197ac6991b98ec91f98495bb3ddb2cba36ce5dfa28a52063a373bb03f5d2e34f6e0c7b6b81b3046a4d7d0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/891d7ed26f62b0f8757b081e9d76840636877f1613ee17b7695fe2ee8640258c56e01291934e3797728bc2300a1b5f41e3f3ef81ab532c0e5ab475bb9b6f097a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/8926397375b8328137652c18cd4371808b214cb26864d77dd33c8eea895e10e62c297064c3d4c703d58ee11dee81bbf9851525e3a0c33151f54c164b8a4343b0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/89d29d938e64b1295e138faedfa5df6f6729a67e5ba8e0c3fdd1c9266e0f59a4bc35b8a21e14225072317879af3323fa0457e01d9b08608770bc23957feec6c0 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/95fc2854c0fa2528b03e2bcabc7612f56f0bcb3d6f54a06293e21d1887885a449907b5b320a1a94bac712bf56fdad0a4184b17047c1fc179dfa83acab3f70d21 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/9a58ceae46de4649375b6b880b8500c85d34c8e9bc650dc4c993afb2fa8d45c4180331821c6226a0fb1475b04a0b9a849502647ef9c4dcac74769eb501c885fa +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/a19ae062208aa20b7310705af3d26ef53095a9dabfad080883cc7a32e98687063179db95cb2c71ef9064801c59fa46d261f172d83e83f96a39c274387b59dca4 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/d1169a8d10067493e42752c2a7615ff27f55bd90c38b91feef918958e29a2239ee289a0cde8385441d1f0fe9af1fb634fa6d56438b9f33ec382e81fa59d70b54 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/e171e5c6039c050c2368584a537d36df221b1f9f23d285c0399b95b14608d67006229823e6831bff7d8b0c2f9e86ebaec9c6811461119195f430ade055073fed +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/operations/ee118dd19aac4e1cb354f83a37dadca70bc5f086cc8d36cd0059b222bf8c7250824401ff681518d0120f8db1f96f0025c464dcfc3b1ee28777e8c76325760134 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/type +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/123b0e36150cf8e99d644d2dfd1a7b0c8d2f676a78248d6902516d9ae58903665c79ec3f5f6729b98a1237dcf2abc1d41e690ae0ad30888c84786bcc9de5e314 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/2ac0b7d8b1fdfac82b3ff3926e0018f72ef2b48f85b41f5fa541271370e1197d41e37c2c6d62af0c6974658c4a9e5e945b8efcbcc7748bdd99bd9483f7e13e22 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/67b8396b301935ff624ff98952c57d8ee021e7d885e1220053b993e7bc822a4cf061298e9643ded745c55f3ed8e923a6731524129993d2664f48b60660761145 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/9c6fca696f77383cc87d068fe3f5912466b157dccc6465973e653dd4d2c02e2eca9c0725c071857bf3f4f0e263259eafc18f1739615a501e672bb2afd415316b +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/cae2e3e5952cb5ba93f27e3898d90dac6c41fc9e20c66ef0791e71b91dc103d924996c921c88179705264a224fb5d2bb29091fc8f18f0ea7a088aaabb859ea2a +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/cd8efb6da14127c81c37b56ead18a39b30f2cd154891185a6e906efb491dbf63f290eed3c31d4725f490a93208ffbe5cdc031d5b6de38fcb77cbb11f0357118f +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/e434359d6306a4f6997733b9b5308299984f05219c92e1b2a31f1203126be0fada5fc09a2ba86c98d1318981cb53997c7f8674ef25da6ca7bf9fd849598cd355 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/ea75e7ccb42f52b013dc1b45bb4e6d692b37c5e38bd39356ba2806be83ca5ca03ce20481db2788428126f93663e6723f99f4d46c5c705f5b12b70e2127ab15ba +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/fa5dec7ee06fbc6cbb2798c8e98bab482a6750776de41406fb06893c83f71f5015ec7ee2873642714b7bbc1c496f880e3acf44ffdabf2f87a39e0fbd68a4cc46 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/op_store/views/ffde172c6285c71851b22780e34962d8f9234067af59ff2dc38b74e905dc540fe35b944e8c0b2d4230dfcf778424a335494cd0aeea1bb4be2a11c4b5428ad465 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/14e3f15273e204cadfe38dd2f38cebd1343a6bfbb91c6af0f5f9de6e9003d8ec8b0eb676975af9dca2d06ee5e6b3886c3c5d3755c6f149b6b09172cccc35adff +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/42e0f32dd10ba6ae9f2297ca8ad0bd16337f14545b29f956ce380a7ab92bab771cbc9e04755752a6fa13231286f724379d6662b0fb257ef2cc2c52fe680eb95d +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/466a441e56afdde383cef1d6127ca1d4c825157feb65a59f8f6ff5fa7a523bd683d0b7a6bb16c00afff53a890319a472d98da9af3dea5675168a4d424aa7af32 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/482ae5a29fbe856c7272f2071b8b0f0359ee2d89ff392b8a900643fbd0836eccd067b8bf41909e206c90d45d6e7d8b6686b93ecaee5fe1a9060d87b672101310 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/5403fa06049419ddcf620ce0dc20911583e1b1062b42630ee325aa5ac2f918dd266dfed93ebc808a2aecb37ac3a33e251fe1396fd24b8ed566bfbd61a81ff959 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/56119fa0480cb66159978b1a8c9b031f9e978b7a3172eb87407a701fea34fdd90db7771cd433a2cf7e8a84a7b49c924f973eccb0eb05685ec42f36f7ef61cc06 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/90b1f4de4ba65e0652f1de45e4c84b623f99bd9d9453667e19c7040857bd397e59e7a84406f2ab6204d25789d995c47c350d8abc47a4bbb102059f3111f20028 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/9215bd4ac28fdecba111c63bace46d0f1c253ad3af44e0e74bd43d30757bef2e655538c1172c8f25769bce0a3c669b713440773c1859f0a136d9ca42501f2470 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/a79320f784ef97b3d6297e55a48b17a517a38d95d5c61ba8d01c59d68dcd2ccf3a96479f4fc3c4cafdcd56dc7bd58b1cb987e079764c1646533ab32418900727 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/abe6b7a350e1604cb8f5a2cd10cef13a019bd797770e6dc37414d33d98dcf36d8ec80a2ae13bd7dd2d2076772c0ffadec0b53ba72f4669b461fff2da5d30f1ba +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/b7689d54193a3798edd58d758966ad65ad57297c5276eab3e4ef07380779363efe9e462a149f4d42f55bbe004eb5ba88bf35df4c78ac975275530382390159d6 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/e75f5f3431d172d7e9434dfaef2be50812105b3ead73eeb10345c9b6892e9cbb5ee0602ebb0ceaf5ab87d22f45930dc30d136d0aac77310fb0261b3857ffde9b +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/extra/heads/56119fa0480cb66159978b1a8c9b031f9e978b7a3172eb87407a701fea34fdd90db7771cd433a2cf7e8a84a7b49c924f973eccb0eb05685ec42f36f7ef61cc06 +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/git_target +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/store/type +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/submodule_store/type +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/repo/workspace_store/index +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/working_copy/checkout +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/working_copy/tree_state +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/.jj/working_copy/type +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/LICENSE +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/Makefile +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/README.md +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/cli-ref.md +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/cloud.md +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/gen_assets.sh +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/docs/index.md +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/__init__.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/__init__.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/avro.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/base.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/csv.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/formats/jsonl.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/handlers/__init__.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/handlers/base.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/handlers/cli_table.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/__init__.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/aws.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/az.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/base.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/fsspec.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/gcloud.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/storage/local.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/style.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/src/tab_cli/url_parser.py +0 -0
- {tab_cli-0.1.5 → tab_cli-0.1.6}/tests/__init__.py +0 -0
- {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.
|
|
@@ -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.
|
|
@@ -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
|
|
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[
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
104
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
200
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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 = [
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
[
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|