mirror of
https://github.com/kennethreitz/tablib.git
synced 2026-06-05 15:00:19 +00:00
Compare commits
894 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f54811c7 | |||
| e8774043ed | |||
| dc1729fc6f | |||
| 3dc62685f8 | |||
| 22c88de90d | |||
| 615e308559 | |||
| 8c5404591b | |||
| 5fa4496f9d | |||
| bc8438bda4 | |||
| ce79e44d14 | |||
| 985c3d98b0 | |||
| 6d097c0214 | |||
| 16b5565354 | |||
| c25fe54b6f | |||
| b39aefb8d8 | |||
| a442758729 | |||
| 21479001a7 | |||
| f7e39c1ad5 | |||
| aaeb5c8360 | |||
| 7a6c623cca | |||
| 0c31fcb3e4 | |||
| fa7fdb0443 | |||
| 8e19479cea | |||
| 8f39ac5055 | |||
| 8d02934c53 | |||
| d0963c206f | |||
| 993af5b0b4 | |||
| 0accb4c437 | |||
| 0821716983 | |||
| 660990b6b0 | |||
| 6152d995f0 | |||
| 0ea6d706a9 | |||
| 00d8ab0b37 | |||
| 06c2326dc0 | |||
| fa30ea858d | |||
| 4de2e17984 | |||
| 52b64757b7 | |||
| 5ff4a55ae6 | |||
| ce7d887adc | |||
| 57a535f577 | |||
| 357a5594c5 | |||
| f61b8d8926 | |||
| 22a193dafb | |||
| b539e96697 | |||
| 626a062747 | |||
| 9d2f7d6999 | |||
| a9d9671b7f | |||
| f1046cd13e | |||
| d21bd10908 | |||
| c26159d48f | |||
| 34fe72305e | |||
| d94420d968 | |||
| 51a720b21c | |||
| 20f51d0bc1 | |||
| 87d15a1529 | |||
| 08a6759520 | |||
| 205403d377 | |||
| 9858539c87 | |||
| 201d8d9910 | |||
| fede4a4f13 | |||
| a76933edd5 | |||
| 3197e59b25 | |||
| 1f000f2f2c | |||
| 7879fef65a | |||
| b8bff1190e | |||
| d77aba6210 | |||
| bf6e5c2e78 | |||
| 088b916bab | |||
| e4ac50260e | |||
| 7347d07624 | |||
| c9027b446c | |||
| 825de0193b | |||
| 78b483d39e | |||
| 8f09789d40 | |||
| e0e75ed43c | |||
| bdc84255a8 | |||
| b3c7145c40 | |||
| 44f43516a5 | |||
| e0a40577fd | |||
| 0329eb6168 | |||
| 4c3dc847b0 | |||
| 89fbd54b00 | |||
| 067dc769dc | |||
| 1726c1cf37 | |||
| debe77e432 | |||
| 0e022d89e5 | |||
| a40852d1c6 | |||
| 9b9fb0aa8a | |||
| ca1aa3ad30 | |||
| 2cfde95fe2 | |||
| 5b94682df3 | |||
| f6bf14afd2 | |||
| f3d02aa3b0 | |||
| ca8dbcf9be | |||
| 4418535030 | |||
| 5bd896b954 | |||
| af414b69d7 | |||
| 91062672b5 | |||
| 34334e72a1 | |||
| 5595bb7993 | |||
| 5fde5259d9 | |||
| 591e8f7448 | |||
| 4dfe2c2f89 | |||
| 91608895d6 | |||
| 0d36390254 | |||
| 8ea082ce60 | |||
| a0df54ca22 | |||
| 0e06b7e328 | |||
| 8aeb5e5158 | |||
| a0d19a56cb | |||
| 8cc024e61b | |||
| a7c40a0881 | |||
| 20de7fad98 | |||
| 4969a71f7f | |||
| 743776371a | |||
| 326d07c2ed | |||
| 2f6ea8c644 | |||
| 8aaed50cc8 | |||
| a21b276d9c | |||
| e8838b5ce6 | |||
| 923711d99a | |||
| aac129db66 | |||
| d9df89f5da | |||
| 22bb20c74b | |||
| d25d24a9bb | |||
| 513bba2c20 | |||
| 2b9ce02e3c | |||
| f9f28d3d86 | |||
| 0cb50bb008 | |||
| f55f56ae1d | |||
| 0937c9f9ec | |||
| 25a66f95ac | |||
| 6ab511f8c0 | |||
| 64816258e6 | |||
| 41cbaa04b9 | |||
| c136940801 | |||
| cf03ecfe25 | |||
| 193b840da2 | |||
| 733d77ad1e | |||
| 3abd7e8c53 | |||
| 0be9e6a74b | |||
| ecd0afbcec | |||
| addaa090ef | |||
| f7b3fd4601 | |||
| 79dc77de49 | |||
| b057cdf05e | |||
| fc2f3c07c8 | |||
| a10327a283 | |||
| e0de42ef06 | |||
| f757ab84d1 | |||
| dc24fda415 | |||
| 3ba8d529fc | |||
| a8bdb4b28f | |||
| 1aaf235751 | |||
| 36ec60d5dd | |||
| babcbfd949 | |||
| 29b2c08da0 | |||
| 862a681263 | |||
| 102073c426 | |||
| 499ce52304 | |||
| c650b67e06 | |||
| 3e4d6fb5aa | |||
| dd2ba714d3 | |||
| a28a057559 | |||
| d38549ef1e | |||
| 5a359ba4de | |||
| 359007444c | |||
| 4f8949417e | |||
| 3d5943a8a4 | |||
| 38486231cc | |||
| 75f1bafd69 | |||
| 4749760e6f | |||
| ac3cf67620 | |||
| f812c29275 | |||
| 4c5d0b1a45 | |||
| 61063e2b09 | |||
| 4c300e65a5 | |||
| edbb16ec97 | |||
| dec5cea722 | |||
| 38183938dc | |||
| 7f1db4023f | |||
| b09fface1b | |||
| 69edb9def3 | |||
| ec54918f4a | |||
| ab6633549f | |||
| 56005d8022 | |||
| 36fa7ef097 | |||
| bb0abc863e | |||
| 58f6eefe01 | |||
| e4726cb85c | |||
| 412e690289 | |||
| 44e797d70e | |||
| 34c14aca18 | |||
| 7c318adde4 | |||
| 5dd74c0104 | |||
| a50ff92ff2 | |||
| 383d4b9c4e | |||
| 00e2ffa2ef | |||
| a3cd2c9cff | |||
| d89d243a30 | |||
| 69abfc3ada | |||
| 80e72cfa27 | |||
| 05bd0d1d42 | |||
| 62807734bd | |||
| c5c2dffe42 | |||
| 46102d4be7 | |||
| 44e9e24fec | |||
| 0ca5520bbc | |||
| e66eb4a189 | |||
| 0e720d78ca | |||
| 6afe716d64 | |||
| 76cbf9fadf | |||
| a93f93a458 | |||
| 3d44bdec40 | |||
| 319505817a | |||
| 6cb9a69746 | |||
| bb1354b61f | |||
| ddc4bd30f2 | |||
| 52e547daf9 | |||
| 7f0b7a0a22 | |||
| ddac443732 | |||
| e13f4d0aba | |||
| 54f9041f2c | |||
| 91d3299280 | |||
| cd67a63b43 | |||
| 19b3d6d06a | |||
| 59090d33a8 | |||
| a4f974287b | |||
| f59abe84be | |||
| cf23f2344f | |||
| e16bb38c48 | |||
| 71ca275dd1 | |||
| 75bbfbbaf4 | |||
| b35d505621 | |||
| cd491c062c | |||
| 9fdb72cc5c | |||
| a5b1f7987e | |||
| 8cf6770a76 | |||
| 5fa3d2f886 | |||
| d4c66c7a4e | |||
| af17586581 | |||
| 23d21f00f3 | |||
| 7ee924b5a6 | |||
| d720beadac | |||
| ee9666a146 | |||
| 77a9e25795 | |||
| d515724817 | |||
| 2814fbc381 | |||
| 9ca1d4ec54 | |||
| abbb4e32d8 | |||
| f6e757d569 | |||
| 9ba0451843 | |||
| d99db57d75 | |||
| 2299c00883 | |||
| 5ba6f5d91a | |||
| bbdf5f11ab | |||
| 851ba25702 | |||
| 039272b274 | |||
| d6a7832e60 | |||
| e51c4faec7 | |||
| f7fc3244ee | |||
| 53d69bd3ea | |||
| fcc9700d11 | |||
| 1ec9c18a66 | |||
| 99c28fa560 | |||
| fa7fb579fd | |||
| be24de19dc | |||
| 1d4f4b68ca | |||
| 8debeb26ac | |||
| 38e1ee6c3d | |||
| a774789252 | |||
| 995eabad37 | |||
| d90358bf69 | |||
| c5920249de | |||
| 9b6a73c97c | |||
| 679bd115b6 | |||
| 32cbc36fc1 | |||
| 8bded88559 | |||
| f8f57a467e | |||
| a11a993955 | |||
| 25894f2948 | |||
| 591b89693e | |||
| 85d9c2497e | |||
| eaf52b691e | |||
| 6f53c5d2b9 | |||
| 90ee799576 | |||
| c02a21ccd2 | |||
| fa045ca114 | |||
| 65703550c3 | |||
| 1fcb98f9ae | |||
| e2d45ecff7 | |||
| 47d92277cc | |||
| fdd74b5b0c | |||
| de052f0fac | |||
| 2f3acf5af4 | |||
| c4e8755cd2 | |||
| 79dc4524a0 | |||
| a785d77901 | |||
| b3485ec942 | |||
| 28b358c9da | |||
| 24657520e9 | |||
| 66d9e50984 | |||
| 541fba6786 | |||
| bc6398ffb0 | |||
| dca7bc9a7d | |||
| 2fbda0f43d | |||
| e350f9428b | |||
| 68dba0a77d | |||
| 028be03c2c | |||
| e1d65ba3c8 | |||
| e4cb3bcd9b | |||
| bf9510e0c7 | |||
| 82ae3ca507 | |||
| 5fbdd56fba | |||
| f187cef5f4 | |||
| 87892d7266 | |||
| 20e2ce5ba0 | |||
| 48e576954d | |||
| a21f8187f8 | |||
| 8479df725e | |||
| 333deb2311 | |||
| 0b714f21e1 | |||
| ae730b00b1 | |||
| 84e8b0384f | |||
| 7a2842a8af | |||
| 954bbdccf3 | |||
| 7acaa8460d | |||
| 84e7e251ae | |||
| dc868eff31 | |||
| 43356e908c | |||
| f7acc19523 | |||
| c5972db8f0 | |||
| 1cc051f3e8 | |||
| 3da155ce0d | |||
| 9a34cf0980 | |||
| 434f66b4eb | |||
| d056916c53 | |||
| cf5239f097 | |||
| 49d8cb816f | |||
| fbd277ff2e | |||
| 6f4572fa56 | |||
| 453fc8614c | |||
| 01cf58e431 | |||
| f6cd89c76c | |||
| 1e0f30e8a6 | |||
| 569d35bfca | |||
| d40cdfbcd0 | |||
| 86bbaf9bea | |||
| 0ed01d85b9 | |||
| fc4cc7fa14 | |||
| 70716fdd21 | |||
| 1146ec2341 | |||
| 1a7d597745 | |||
| 56b627a561 | |||
| 98e182bed2 | |||
| c8a5563309 | |||
| c225a64d68 | |||
| d611d5a14f | |||
| 45121ddd65 | |||
| c74357cb20 | |||
| 939b0af551 | |||
| 9c2018653f | |||
| 2bc6122ee8 | |||
| 7f0748aac9 | |||
| 41a5c67159 | |||
| 3efefcc8da | |||
| d19de6025b | |||
| 65ba937c0d | |||
| 79a2bb888f | |||
| 25eacaf6f0 | |||
| c2a9af7fb3 | |||
| 3b06f3760d | |||
| e7ee3195a7 | |||
| 5bd2e3df52 | |||
| 837b3f83e6 | |||
| ff8f23edd5 | |||
| 5ffcfd56f2 | |||
| 955c24c974 | |||
| 192a5efabb | |||
| 1aafc7e2f4 | |||
| 9e45b95d12 | |||
| d8f0a018ae | |||
| 7545f3726e | |||
| 85e2bd73fc | |||
| 37033903c5 | |||
| 02c38c2520 | |||
| 26748deb9f | |||
| 63f6cea132 | |||
| 1b035f9774 | |||
| 2c14486c33 | |||
| 8bc69c9d85 | |||
| d36a2cbd42 | |||
| 1ab0eb3fae | |||
| cd71e1a5b1 | |||
| 47f79a7ca1 | |||
| 9f38efe413 | |||
| 5d98239a7e | |||
| a3f0d02633 | |||
| b29007a0df | |||
| e75c3c1a66 | |||
| 47cebbc328 | |||
| e4c39524f7 | |||
| c88c794314 | |||
| 752443f077 | |||
| 7c0507bcce | |||
| 652ac85549 | |||
| 05ea3c35fc | |||
| d5fada7e1d | |||
| 511c58d4e1 | |||
| c469360a0e | |||
| 97b4401b18 | |||
| 40e0f41b4c | |||
| 39435727ba | |||
| eda9d5af03 | |||
| 15435047c6 | |||
| a3781e3c89 | |||
| 6a825a8a39 | |||
| 6a449d497a | |||
| d807c60346 | |||
| 71603662b1 | |||
| 21c11b9911 | |||
| e8c923d712 | |||
| bc581c08df | |||
| 4f9c9d09ec | |||
| 63e8a7172d | |||
| 45e0af9f0e | |||
| fa6f5b3af3 | |||
| 0528e0a500 | |||
| 8e83734985 | |||
| 783eccc67d | |||
| 7236415f42 | |||
| c0a3c3ea1e | |||
| 14bd964fb1 | |||
| 6bfc6634ba | |||
| 54affad292 | |||
| 7c963a0f4d | |||
| 02f27f15c5 | |||
| 9c65515e7a | |||
| c87a954a9e | |||
| 42e40ed0ab | |||
| 23ab6c4724 | |||
| 32a09ccd6a | |||
| 81a7f79b3d | |||
| 05c9b33003 | |||
| ec7273d02d | |||
| 19ee1997b5 | |||
| f01d65c2e9 | |||
| 9778a96351 | |||
| 906138b138 | |||
| 43c68b396f | |||
| d611233c80 | |||
| 3d02b866ce | |||
| 887ee2fbac | |||
| bfd211854a | |||
| bc75911500 | |||
| a2b4e4c6ba | |||
| fde6f11763 | |||
| 33a83316df | |||
| f6d7888d9e | |||
| c19e2f2c5b | |||
| eaa2b9b8ea | |||
| 2f8083bda6 | |||
| 2c5a9af76e | |||
| e74a8f41cc | |||
| cd5aa4fc06 | |||
| 1d460bac40 | |||
| 4a3fde37a3 | |||
| 62ad123ad8 | |||
| fefc7b4d1f | |||
| 6313437a27 | |||
| 23a5bb1443 | |||
| 864f29cc4b | |||
| c136b794a7 | |||
| d254c2d2b0 | |||
| 9b235150cf | |||
| 9f3e6eeaa1 | |||
| 51728f954f | |||
| 2949b7c656 | |||
| 07d243bbc9 | |||
| bf3484e606 | |||
| 9b2ab6fae9 | |||
| 7a3d55daab | |||
| eec0595c5c | |||
| 0c7c248b96 | |||
| 0d14f7f2b9 | |||
| d5f713024d | |||
| 415bc819e7 | |||
| 974258094e | |||
| ab16f69be6 | |||
| 28d9af852a | |||
| 39c6ea6503 | |||
| 39b66ad8e9 | |||
| 004b3da680 | |||
| d4923533eb | |||
| 29e0b76910 | |||
| 4f54de2630 | |||
| 1f0d68ee79 | |||
| cae8fa1276 | |||
| 4c0a20a7b9 | |||
| 6c1fa87138 | |||
| 0e30255836 | |||
| 1156d5a220 | |||
| 83b71967b9 | |||
| 4dab48cd76 | |||
| 5324526329 | |||
| 1dfcd42233 | |||
| f162b19bd6 | |||
| 707164e459 | |||
| 42f0a285c3 | |||
| d111cc7cc7 | |||
| 25fe211a22 | |||
| 4b675494c4 | |||
| a196b9a5dd | |||
| 5ba56c2bb3 | |||
| 36fbdda492 | |||
| 273d2729ee | |||
| 3036bc9e52 | |||
| b9c74eacc8 | |||
| 805ccfae34 | |||
| fddc018394 | |||
| 2477100062 | |||
| 983b979fda | |||
| 3edb45bac7 | |||
| 29d626fa1f | |||
| 1f22fc7321 | |||
| 8631f60f8d | |||
| 65873b6112 | |||
| 56e44bd45c | |||
| 87e65fd3e7 | |||
| ffbc3b122d | |||
| 9d71603dad | |||
| cceb41af98 | |||
| 60ffa898fd | |||
| a4a211b5a6 | |||
| c9766a48b0 | |||
| 6975685b89 | |||
| e920244a1b | |||
| ea63779baf | |||
| d826f6d0ae | |||
| f6fa3f2abc | |||
| eed6df45e0 | |||
| cb4c67767a | |||
| 1e21fee70e | |||
| 420dd36ab8 | |||
| 9a05770899 | |||
| 8e055f1c57 | |||
| 239e33aaed | |||
| bf4fdea187 | |||
| 03086052ed | |||
| 2128473938 | |||
| 74c64d66a9 | |||
| a4e77f22c4 | |||
| 2e03046a07 | |||
| 06a7b4cd4e | |||
| 6a70b84166 | |||
| 77d9fe8b41 | |||
| 64cb547e0a | |||
| 9146de36d4 | |||
| 9761ff5e9e | |||
| e5259cbb58 | |||
| 56ef89424f | |||
| 4a01299293 | |||
| 9399bf2fe7 | |||
| cbdaa09e83 | |||
| f30e760657 | |||
| a60e2f132e | |||
| 2b36d71554 | |||
| 690de63b7c | |||
| 6b6ef70c61 | |||
| 322283b8f9 | |||
| 3968729903 | |||
| 7b1e533e39 | |||
| 8dd7d73abc | |||
| 176c9615d6 | |||
| c65fd4201f | |||
| 11bca4f7a2 | |||
| 2b5818598a | |||
| 79fb82d69d | |||
| 5350355fbe | |||
| 85673b365c | |||
| 87ce64d4c8 | |||
| 2cd381389c | |||
| 35f21cf73e | |||
| 0ebc8f5e1b | |||
| 865ce62782 | |||
| 3b961c59e7 | |||
| 4be341be4f | |||
| 2c4337b317 | |||
| 0e4128c73e | |||
| 4ebd66cb09 | |||
| bfcfa37ebb | |||
| 5c50c1822e | |||
| 2e5577ee91 | |||
| 84e4bd9a47 | |||
| 7270ce49e1 | |||
| c3052cc02c | |||
| 999c49a4f0 | |||
| 59c996f9df | |||
| a2b62669b7 | |||
| 15e25ef735 | |||
| 7ae7d3ff46 | |||
| 69ed718191 | |||
| 328d3880d5 | |||
| ea3cc847a0 | |||
| 8efab51355 | |||
| e42d215833 | |||
| 10bc5549c9 | |||
| 1a5e2ecb33 | |||
| e1bf189847 | |||
| 0785328e21 | |||
| 6ba0cc9af3 | |||
| 36876205e7 | |||
| 1b97b7191e | |||
| 8b575df419 | |||
| 6a3928759a | |||
| 63348d883b | |||
| 5dce600969 | |||
| 0913b54f47 | |||
| c5bbc74b96 | |||
| 7f5342a1b8 | |||
| d42f9bc10f | |||
| c6565c9e29 | |||
| 1a9343750e | |||
| 8a393214c8 | |||
| b8ed741a36 | |||
| cddbd78a61 | |||
| b113f49ce6 | |||
| 1429b9f8c4 | |||
| 42700f98a5 | |||
| 0e56db632a | |||
| b07512071e | |||
| e4881809d6 | |||
| 54ab300d2d | |||
| 4368d64317 | |||
| 117344de14 | |||
| 58bc1c7dcf | |||
| 4c8b5e72e3 | |||
| b900236157 | |||
| dc14a16e04 | |||
| 2d2ac9b708 | |||
| 1efcb7a63d | |||
| 65c73dfc42 | |||
| 3803a7a21b | |||
| 8b5b29fc90 | |||
| e8ba765426 | |||
| 57001a5465 | |||
| c8493ff047 | |||
| 03914323c2 | |||
| 2f331cee8e | |||
| e1734f2315 | |||
| 22cddbcd63 | |||
| 76f09cd3b3 | |||
| d11c09febe | |||
| 9ab277a468 | |||
| 23c1831144 | |||
| 1cf9bd14b4 | |||
| c2331f7a23 | |||
| bccf0d1ba1 | |||
| c219972ccd | |||
| 52e9d44739 | |||
| e94ecd8472 | |||
| 96067e6380 | |||
| 1cc0f7d1f4 | |||
| f685bf548e | |||
| ca336926da | |||
| 1aa3d3b06a | |||
| be576135b2 | |||
| 0c05d0497e | |||
| 52e307ea35 | |||
| 5cac9bd97e | |||
| a285e993f1 | |||
| 0ed367a31c | |||
| c4815c24cc | |||
| 20fe1e0153 | |||
| 5db8d1c3a6 | |||
| 828017f9a7 | |||
| cff8a6ac9a | |||
| aa8590e8b8 | |||
| d2de647c47 | |||
| 7afef680f5 | |||
| 35763f8c24 | |||
| cc3d020914 | |||
| b8b5405f1c | |||
| b7aebbc74f | |||
| d776d78df5 | |||
| 6f9365d376 | |||
| 621b1bd45c | |||
| be21b6fadd | |||
| 832bfbbb1b | |||
| 288b15fb54 | |||
| 73df22303b | |||
| 4c125bd206 | |||
| ff0de1377a | |||
| ccb29c68fa | |||
| e077a7f2bc | |||
| dcc52bdc18 | |||
| 9cac54eefc | |||
| f69a96f07e | |||
| ca77ed6f64 | |||
| 806aba9ef3 | |||
| 23cbc0c333 | |||
| 34ab54de77 | |||
| 0843a15879 | |||
| 08ed309382 | |||
| 26b6faa88d | |||
| 140736ff33 | |||
| 5379c5683d | |||
| e8b44b5777 | |||
| a0822bc9b0 | |||
| 89b431213b | |||
| 695e8c5af7 | |||
| 0797ec67d4 | |||
| 1852624a7e | |||
| f81dc41a57 | |||
| 34415b89b8 | |||
| d25655588b | |||
| 22c4d185e1 | |||
| e3b3659ea4 | |||
| 22d337790a | |||
| 0784d4b32c | |||
| 332c5bccd9 | |||
| 7055d18a2e | |||
| 6a7c685111 | |||
| 0e5b8f7058 | |||
| e3e6b656e3 | |||
| 99896a5f28 | |||
| 25da44f569 | |||
| 7727171379 | |||
| 91bd4eb9c7 | |||
| 9b74b139fd | |||
| 823a543f41 | |||
| 1aa275bf99 | |||
| 17bb0d3b2c | |||
| 1a9aee9289 | |||
| 196edb82cc | |||
| a2990d5852 | |||
| d992ece86a | |||
| 46f302255d | |||
| 9e3ab4c13f | |||
| eaed0e48c2 | |||
| 501187b357 | |||
| ea4aef88b6 | |||
| 24d800fac3 | |||
| d8136ab613 | |||
| 36bbe2726b | |||
| 1427be2901 | |||
| 10ce000d31 | |||
| a91254117c | |||
| b67762604f | |||
| 83a8346e8f | |||
| 657ab98d04 | |||
| 9ddb4de942 | |||
| 5fad80a540 | |||
| cabab73045 | |||
| 2bb0525990 | |||
| f364bb576e | |||
| 09d057094e | |||
| 8082c4ad43 | |||
| 00e9ae0120 | |||
| f01c22213e | |||
| a58bf269d9 | |||
| 437a135dd3 | |||
| 0409ff50af | |||
| dd24edcc24 | |||
| cf28f4baa8 | |||
| 52dcf79c41 | |||
| 49f098ee22 | |||
| 642b1d8def | |||
| f6964bba8f | |||
| 8d6e75ad20 | |||
| 30487999ba | |||
| b74308e81e | |||
| 577289cbc3 | |||
| cf10703e31 | |||
| 778ad0265e | |||
| e3dedb8887 | |||
| c6e240fa52 | |||
| 5c747c9c2e | |||
| 0bbd990ed8 | |||
| fcada243a2 | |||
| fca8ad6182 | |||
| 35d9e390fd | |||
| 8ca180c461 | |||
| ff63558a67 | |||
| f621b56178 | |||
| 2b529bcb1c | |||
| 90c3435600 | |||
| 1fa28ee2ca | |||
| a5cae7c249 | |||
| 666991ca1e | |||
| 5f4162918f | |||
| b554ce36bb | |||
| e5e22d3ca2 | |||
| 8626351618 | |||
| cdfacb6d6e | |||
| 108c9de130 | |||
| 271aeebf56 | |||
| e75a00541d | |||
| 3b0e0c7991 | |||
| 23440fb7a3 | |||
| 459f310857 | |||
| f9021f53c2 | |||
| 7fda829d27 | |||
| ca08ac8a7b | |||
| 08b51113d3 | |||
| 3e391fc8e3 | |||
| a230844914 | |||
| bc82be09c5 | |||
| ed9fe01604 | |||
| e69546a0ff | |||
| d4b659ece9 | |||
| 55eb3f93e3 | |||
| be7182aea9 | |||
| 48def2cba6 | |||
| df8c0335d1 | |||
| d0b09f0fce | |||
| 9efd982bfa | |||
| a3c82804cd | |||
| 2e75e93f57 | |||
| a26d782e88 | |||
| f5c0c5c34d | |||
| d9aee8e605 | |||
| 315a082b70 | |||
| 120ce9fcd6 | |||
| 914a82eac9 | |||
| 3931bcb4e6 | |||
| 471e56c387 | |||
| 8553dbc040 | |||
| 96c93871cf | |||
| a54949bc08 | |||
| ed686c2391 | |||
| 143677be77 | |||
| b2c35c2543 | |||
| 9dfd9d0c8e | |||
| 140e23c980 | |||
| ac797f1eda | |||
| 7c90595364 | |||
| 38ac98fdb2 | |||
| 07c7d172d9 | |||
| 9c7707be60 | |||
| 14bee65208 | |||
| 4fc5e0655d | |||
| da2e670d0d | |||
| 5912bf4870 | |||
| 28e9d7e23e | |||
| 930d38cf5a | |||
| 5e433c263d | |||
| 19ac9b9716 | |||
| 6feb59504a | |||
| 817eedd6f5 | |||
| 4d1c5a9996 | |||
| 520a1986d7 | |||
| 1ea793112c | |||
| 41a7a5d329 | |||
| c4edaa2ca8 | |||
| c612bb3dae | |||
| c223dfbdf1 | |||
| 49bd48b016 | |||
| c6d90bc825 | |||
| bcd0e37a65 | |||
| 8c92e878a3 | |||
| da2b011358 | |||
| a8b0bf4b5f | |||
| 6574d3e58b | |||
| 1020799828 | |||
| 333e73f892 | |||
| bfe70066b8 | |||
| fbfbe01b70 | |||
| 06a394ea5c | |||
| 9427decdb0 | |||
| fb59035f8d | |||
| 187d12cffc | |||
| eaa4de7793 | |||
| d479c5735a | |||
| 96668bb393 | |||
| b369baba40 | |||
| 25f846a78a | |||
| 22fe18239f | |||
| 149bafa97b | |||
| 9f7fec2379 | |||
| 762ac39e27 | |||
| 2a7aa959b3 | |||
| d85523b6a6 | |||
| 25a5bcea0c | |||
| f58d4b67dc | |||
| a310ab7a09 | |||
| 7f2f925ddb | |||
| 3fc898e222 | |||
| de46f45e2e | |||
| 392eaac299 | |||
| 3a9c3944cf | |||
| 8c402da729 | |||
| 8feb6e8ddf | |||
| 9072b6ddae |
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# .coveragerc to control coverage.py
|
||||||
|
|
||||||
|
[report]
|
||||||
|
# Regexes for lines to exclude from consideration
|
||||||
|
exclude_lines =
|
||||||
|
# Have to re-enable the standard pragma:
|
||||||
|
pragma: no cover
|
||||||
|
|
||||||
|
# Don't complain if non-runnable code isn't run:
|
||||||
|
if __name__ == .__main__.:
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[](https://jazzband.co/)
|
||||||
|
|
||||||
|
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide
|
||||||
|
by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the
|
||||||
|
[guidelines](https://jazzband.co/about/guidelines).
|
||||||
|
|
||||||
|
If you'd like to contribute, simply fork
|
||||||
|
[the repository](https://github.com/jazzband/tablib), commit your changes to a feature
|
||||||
|
branch, and send a pull request to `master`. Make sure you add yourself to
|
||||||
|
[AUTHORS](https://github.com/jazzband/tablib/blob/master/AUTHORS).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Docs and lint
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
env:
|
||||||
|
- TOXENV: docs
|
||||||
|
- TOXENV: lint
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
|
- name: Get pip cache dir
|
||||||
|
id: pip-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
|
key:
|
||||||
|
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ matrix.os }}-${{ matrix.python-version }}-v1-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install --upgrade tox
|
||||||
|
|
||||||
|
- name: Tox
|
||||||
|
run: tox
|
||||||
|
env: ${{ matrix.env }}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: github.repository == 'jazzband/tablib'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: Get pip cache dir
|
||||||
|
id: pip-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
|
key: release-${{ hashFiles('**/setup.py') }}
|
||||||
|
restore-keys: |
|
||||||
|
release-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install -U pip
|
||||||
|
python -m pip install -U setuptools twine wheel
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
python setup.py --version
|
||||||
|
python setup.py sdist --format=gztar bdist_wheel
|
||||||
|
twine check dist/*
|
||||||
|
|
||||||
|
- name: Upload packages to Jazzband
|
||||||
|
if: github.event.action == 'published'
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
user: jazzband
|
||||||
|
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
|
||||||
|
repository_url: https://jazzband.co/projects/tablib/upload
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||||
|
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Get pip cache dir
|
||||||
|
id: pip-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
|
key:
|
||||||
|
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ matrix.os }}-${{ matrix.python-version }}-v1-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install --upgrade tox
|
||||||
|
python -m pip install -e .
|
||||||
|
|
||||||
|
- name: Tox tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
tox -e py
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
+24
@@ -17,3 +17,27 @@ profile
|
|||||||
|
|
||||||
# vi noise
|
# vi noise
|
||||||
*.swp
|
*.swp
|
||||||
|
docs/_build/*
|
||||||
|
coverage.xml
|
||||||
|
nosetests.xml
|
||||||
|
junit-py25.xml
|
||||||
|
junit-py26.xml
|
||||||
|
junit-py27.xml
|
||||||
|
|
||||||
|
# tox noise
|
||||||
|
.tox
|
||||||
|
|
||||||
|
# pyenv noise
|
||||||
|
.python-version
|
||||||
|
tablib.egg-info/*
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
||||||
|
|
||||||
|
# setuptools noise
|
||||||
|
.eggs
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# generated by setuptools-scm
|
||||||
|
/src/tablib/_version.py
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.7.3
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: ["--py36-plus"]
|
||||||
|
|
||||||
|
- repo: https://github.com/PyCQA/isort
|
||||||
|
rev: 5.6.4
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
additional_dependencies: [toml]
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
|
rev: v1.7.0
|
||||||
|
hooks:
|
||||||
|
- id: python-check-blanket-noqa
|
||||||
|
- id: rst-backticks
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v3.3.0
|
||||||
|
hooks:
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
@@ -1,13 +1,32 @@
|
|||||||
Tablib is written and maintained by Kenneth Reitz and
|
Tablib was originally written by Kenneth Reitz and is now maintained
|
||||||
various contributors:
|
by the Jazzband GitHub team.
|
||||||
|
|
||||||
Development Lead
|
Here is a list of passed and present much-appreciated contributors:
|
||||||
````````````````
|
|
||||||
|
|
||||||
- Kenneth Reitz <me@kennethreitz.com>
|
Alex Gaynor
|
||||||
|
Andrii Soldatenko
|
||||||
|
Benjamin Wohlwend
|
||||||
Patches and Suggestions
|
Bruno Soares
|
||||||
```````````````````````
|
Claude Paroz
|
||||||
|
Daniel Santos
|
||||||
- Luke Lee
|
Erik Youngren
|
||||||
|
Hugo van Kemenade
|
||||||
|
Iuri de Silvio
|
||||||
|
Jakub Janoszek
|
||||||
|
James Douglass
|
||||||
|
Joel Friedly
|
||||||
|
Josh Ourisman
|
||||||
|
Kenneth Reitz
|
||||||
|
Luca Beltrame
|
||||||
|
Luke Lee
|
||||||
|
Marc Abramowitz
|
||||||
|
Marco Dallagiacoma
|
||||||
|
Mark Rogers
|
||||||
|
Mark Walling
|
||||||
|
Mathias Loesch
|
||||||
|
Mike Waldner
|
||||||
|
Peyman Salehi
|
||||||
|
Rabin Nankhwa
|
||||||
|
Tommy Anthony
|
||||||
|
Tsuyoshi Hombashi
|
||||||
|
Tushar Makkar
|
||||||
|
|||||||
+341
@@ -0,0 +1,341 @@
|
|||||||
|
# History
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Dropped Python 3.5 support
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Added Python 3.9 support
|
||||||
|
- Added read_only option to xlsx file reader (#482).
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Prevented crash in rst export with only-space strings (#469).
|
||||||
|
|
||||||
|
## 2.0.0 (2020-05-16)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- The `Row.lpush/rpush` logic was reversed. `lpush` was appending while `rpush`
|
||||||
|
and `append` were prepending. This was fixed (reversed behavior). If you
|
||||||
|
counted on the broken behavior, please update your code (#453).
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed minimal openpyxl dependency version to 2.6.0 (#457).
|
||||||
|
- Dates from xls files are now read as Python datetime objects (#373).
|
||||||
|
- Allow import of "ragged" xlsx files (#465).
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- When importing an xlsx file, Tablib will now read cell values instead of formulas (#462).
|
||||||
|
|
||||||
|
## 1.1.0 (2020-02-13)
|
||||||
|
|
||||||
|
### Deprecations
|
||||||
|
|
||||||
|
- Upcoming breaking change in Tablib 2.0.0: the `Row.lpush/rpush` logic is reversed.
|
||||||
|
`lpush` is appending while `rpush` and `append` are prepending. The broken behavior
|
||||||
|
will remain in Tablib 1.x and will be fixed (reversed) in Tablib 2.0.0 (#453). If you
|
||||||
|
count on the broken behavior, please update your code when you upgrade to Tablib 2.x.
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Tablib is now able to import CSV content where not all rows have the same
|
||||||
|
length. Missing columns on any line receive the empty string (#226).
|
||||||
|
|
||||||
|
## 1.0.0 (2020-01-13)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Dropped Python 2 support
|
||||||
|
- Dependencies are now all optional. To install `tablib` as before with all
|
||||||
|
possible supported formats, run `pip install tablib[all]`
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Formats can now be dynamically registered through the
|
||||||
|
`tablib.formats.registry.refister` API (#256).
|
||||||
|
- Tablib methods expecting data input (`detect_format`, `import_set`,
|
||||||
|
`Dataset.load`, `Databook.load`) now accepts file-like objects in addition
|
||||||
|
to raw strings and bytestrings (#440).
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed a crash when exporting an empty string with the ReST format (#368)
|
||||||
|
- Error cells from imported .xls files contain now the error string (#202)
|
||||||
|
|
||||||
|
## 0.14.0 (2019-10-19)
|
||||||
|
|
||||||
|
### Deprecations
|
||||||
|
|
||||||
|
- The 0.14.x series will be the last to support Python 2
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Dropped Python 3.4 support
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Added Python 3.7 and 3.8 support
|
||||||
|
- The project is now maintained by the Jazzband team, https://jazzband.co
|
||||||
|
- Improved format autodetection and added autodetection for the odf format.
|
||||||
|
- Added search to all documentation pages
|
||||||
|
- Open xlsx workbooks in read-only mode (#316)
|
||||||
|
- Unpin requirements
|
||||||
|
- Only install backports.csv on Python 2
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed `DataBook().load` parameter ordering (first stream, then format).
|
||||||
|
- Fixed a regression for xlsx exports where non-string values were forced to
|
||||||
|
strings (#314)
|
||||||
|
- Fixed xlsx format detection (which was often detected as `xls` format)
|
||||||
|
|
||||||
|
## 0.13.0 (2019-03-08)
|
||||||
|
|
||||||
|
- Added reStructuredText output capability (#336)
|
||||||
|
- Added Jira output capability
|
||||||
|
- Stopped calling openpyxl deprecated methods (accessing cells, removing sheets)
|
||||||
|
(openpyxl minimal version is now 2.4.0)
|
||||||
|
- Fixed a circular dependency issue in JSON output (#332)
|
||||||
|
- Fixed Unicode error for the CSV export on Python 2 (#215)
|
||||||
|
- Removed usage of optional `ujson` (#311)
|
||||||
|
- Dropped Python 3.3 support
|
||||||
|
|
||||||
|
## 0.12.1 (2017-09-01)
|
||||||
|
|
||||||
|
- Favor `Dataset.export(<format>)` over `Dataset.<format>` syntax in docs
|
||||||
|
- Make Panda dependency optional
|
||||||
|
|
||||||
|
## 0.12.0 (2017-08-27)
|
||||||
|
|
||||||
|
- Add initial Panda DataFrame support
|
||||||
|
- Dropped Python 2.6 support
|
||||||
|
|
||||||
|
## 0.11.5 (2017-06-13)
|
||||||
|
|
||||||
|
- Use `yaml.safe_load` for importing yaml.
|
||||||
|
|
||||||
|
## 0.11.4 (2017-01-23)
|
||||||
|
|
||||||
|
- Use built-in `json` package if available
|
||||||
|
- Support Python 3.5+ in classifiers
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed textual representation for Dataset with no headers
|
||||||
|
- Handle decimal types
|
||||||
|
|
||||||
|
## 0.11.3 (2016-02-16)
|
||||||
|
|
||||||
|
- Release fix.
|
||||||
|
|
||||||
|
## 0.11.2 (2016-02-16)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fix export only formats.
|
||||||
|
- Fix for xlsx output.
|
||||||
|
|
||||||
|
## 0.11.1 (2016-02-07)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed packaging error on Python 3.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.11.0 (2016-02-07)
|
||||||
|
|
||||||
|
### New Formats!
|
||||||
|
|
||||||
|
- Added LaTeX table export format (`Dataset.latex`).
|
||||||
|
- Support for dBase (DBF) files (`Dataset.dbf`).
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- New import/export interface (`Dataset.export()`, `Dataset.load()`).
|
||||||
|
- CSV custom delimiter support (`Dataset.export('csv', delimiter='$')`).
|
||||||
|
- Adding ability to remove duplicates to all rows in a dataset (`Dataset.remove_duplicates()`).
|
||||||
|
- Added a mechanism to avoid `datetime.datetime` issues when serializing data.
|
||||||
|
- New `detect_format()` function (mostly for internal use).
|
||||||
|
- Update the vendored unicodecsv to fix `None` handling.
|
||||||
|
- Only freeze the headers row, not the headers columns (xls).
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- `detect()` function removed.
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fix XLSX import.
|
||||||
|
- Bugfix for `Dataset.transpose().transpose()`.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.10.0 (2014-05-27)
|
||||||
|
|
||||||
|
* Unicode Column Headers
|
||||||
|
* ALL the bugfixes!
|
||||||
|
|
||||||
|
## 0.9.11 (2011-06-30)
|
||||||
|
|
||||||
|
* Bugfixes
|
||||||
|
|
||||||
|
## 0.9.10 (2011-06-22)
|
||||||
|
|
||||||
|
* Bugfixes
|
||||||
|
|
||||||
|
## 0.9.9 (2011-06-21)
|
||||||
|
|
||||||
|
* Dataset API Changes
|
||||||
|
* `stack_rows` => `stack`, `stack_columns` => `stack_cols`
|
||||||
|
* column operations have their own methods now (`append_col`, `insert_col`)
|
||||||
|
* List-style `pop()`
|
||||||
|
* Redis-style `rpush`, `lpush`, `rpop`, `lpop`, `rpush_col`, and `lpush_col`
|
||||||
|
|
||||||
|
## 0.9.8 (2011-05-22)
|
||||||
|
|
||||||
|
* OpenDocument Spreadsheet support (.ods)
|
||||||
|
* Full Unicode TSV support
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.7 (2011-05-12)
|
||||||
|
|
||||||
|
* Full XLSX Support!
|
||||||
|
* Pickling Bugfix
|
||||||
|
* Compat Module
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.6 (2011-05-12)
|
||||||
|
|
||||||
|
* `seperators` renamed to `separators`
|
||||||
|
* Full unicode CSV support
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.5 (2011-03-24)
|
||||||
|
|
||||||
|
* Python 3.1, Python 3.2 Support (same code base!)
|
||||||
|
* Formatter callback support
|
||||||
|
* Various bug fixes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.4 (2011-02-18)
|
||||||
|
|
||||||
|
* Python 2.5 Support!
|
||||||
|
* Tox Testing for 2.5, 2.6, 2.7
|
||||||
|
* AnyJSON Integrated
|
||||||
|
* OrderedDict support
|
||||||
|
* Caved to community pressure (spaces)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.3 (2011-01-31)
|
||||||
|
|
||||||
|
* Databook duplication leak fix.
|
||||||
|
* HTML Table output.
|
||||||
|
* Added column sorting.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.2 (2010-11-17)
|
||||||
|
|
||||||
|
* Transpose method added to Datasets.
|
||||||
|
* New frozen top row in Excel output.
|
||||||
|
* Pickling support for Datasets and Rows.
|
||||||
|
* Support for row/column stacking.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.1 (2010-11-04)
|
||||||
|
|
||||||
|
* Minor reference shadowing bugfix.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.0 (2010-11-04)
|
||||||
|
|
||||||
|
* Massive documentation update!
|
||||||
|
* Tablib.org!
|
||||||
|
* Row tagging and Dataset filtering!
|
||||||
|
* Column insert/delete support
|
||||||
|
* Column append API change (header required)
|
||||||
|
* Internal Changes (Row object and use thereof)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.5 (2010-10-06)
|
||||||
|
|
||||||
|
* New import system. All dependencies attempt to load from site-packages,
|
||||||
|
then fallback on tenderized modules.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.4 (2010-10-04)
|
||||||
|
|
||||||
|
* Updated XLS output: Only wrap if '\\n' in cell.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.3 (2010-10-04)
|
||||||
|
|
||||||
|
* Ability to append new column passing a callable
|
||||||
|
as the value that will be applied to every row.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.2 (2010-10-04)
|
||||||
|
|
||||||
|
* Added alignment wrapping to written cells.
|
||||||
|
* Added separator support to XLS.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.1 (2010-09-28)
|
||||||
|
|
||||||
|
* Packaging Fix
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.0 (2010-09-25)
|
||||||
|
|
||||||
|
* New format plugin system!
|
||||||
|
* Imports! ELEGANT Imports!
|
||||||
|
* Tests. Lots of tests.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.7.1 (2010-09-20)
|
||||||
|
|
||||||
|
* Reverting methods back to properties.
|
||||||
|
* Windows bug compensated in documentation.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.7.0 (2010-09-20)
|
||||||
|
|
||||||
|
* Renamed DataBook Databook for consistency.
|
||||||
|
* Export properties changed to methods (XLS filename / StringIO bug).
|
||||||
|
* Optional Dataset.xls(path='filename') support (for writing on windows).
|
||||||
|
* Added utf-8 on the worksheet level.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.4 (2010-09-19)
|
||||||
|
|
||||||
|
* Updated unicode export for XLS.
|
||||||
|
* More exhaustive unit tests.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.3 (2010-09-14)
|
||||||
|
|
||||||
|
* Added Dataset.append() support for columns.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.2 (2010-09-13)
|
||||||
|
|
||||||
|
* Fixed Dataset.append() error on empty dataset.
|
||||||
|
* Updated Dataset.headers property w/ validation.
|
||||||
|
* Added Testing Fixtures.
|
||||||
|
|
||||||
|
## 0.6.1 (2010-09-12)
|
||||||
|
|
||||||
|
* Packaging hotfixes.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0 (2010-09-11)
|
||||||
|
|
||||||
|
* Public Release.
|
||||||
|
* Export Support for XLS, JSON, YAML, and CSV.
|
||||||
|
* DataBook Export for XLS, JSON, and YAML.
|
||||||
|
* Python Dict Property Support.
|
||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
History
|
|
||||||
=======
|
|
||||||
|
|
||||||
0.8.1 (2010-09-28)
|
|
||||||
------------------
|
|
||||||
* Packaging Fix
|
|
||||||
|
|
||||||
|
|
||||||
0.8.0 (2010-09-25)
|
|
||||||
------------------
|
|
||||||
* New format plugin system!
|
|
||||||
* Imports! ELEGANT Imports!
|
|
||||||
* Tests. Lots of tests.
|
|
||||||
|
|
||||||
|
|
||||||
0.7.1 (2010-09-20)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Reverting methods back to properties.
|
|
||||||
* Windows bug compenated in documentation.
|
|
||||||
|
|
||||||
|
|
||||||
0.7.0 (2010-09-20)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Renamed DataBook Databook for consistiency.
|
|
||||||
* Export properties changed to methods (XLS filename / StringIO bug).
|
|
||||||
* Optional Dataset.xls(path='filename') support (for writing on windows).
|
|
||||||
* Added utf-8 on the worksheet level.
|
|
||||||
|
|
||||||
|
|
||||||
0.6.4 (2010-09-19)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Updated unicode export for XLS.
|
|
||||||
* More exhaustive unit tests.
|
|
||||||
|
|
||||||
|
|
||||||
0.6.3 (2010-09-14)
|
|
||||||
------------------
|
|
||||||
* Added Dataset.append() support for columns.
|
|
||||||
|
|
||||||
|
|
||||||
0.6.2 (2010-09-13)
|
|
||||||
------------------
|
|
||||||
* Fixed Dataset.append() error on empty dataset.
|
|
||||||
* Updated Dataset.headers property w/ validation.
|
|
||||||
* Added Testing Fixtures.
|
|
||||||
|
|
||||||
0.6.1 (2010-09-12)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Packaging hotfixes.
|
|
||||||
|
|
||||||
|
|
||||||
0.6.0 (2010-09-11)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Public Release.
|
|
||||||
* Export Support for XLS, JSON, YAML, and CSV.
|
|
||||||
* DataBook Export for XLS, JSON, and YAML.
|
|
||||||
* Python Dict Property Support.
|
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
Copyright (c) 2010 Kenneth Reitz.
|
Copyright 2016 Kenneth Reitz
|
||||||
|
Copyright 2019 Jazzband
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -16,4 +17,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
include HISTORY.rst README.rst LICENSE AUTHORS
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Tablib: format-agnostic tabular dataset library
|
||||||
|
|
||||||
|
[](https://jazzband.co/)
|
||||||
|
[](https://pypi.org/project/tablib/)
|
||||||
|
[](https://pypi.org/project/tablib/)
|
||||||
|
[](https://pypistats.org/packages/tablib)
|
||||||
|
[](https://github.com/jazzband/tablib/actions)
|
||||||
|
[](https://codecov.io/gh/jazzband/tablib)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
_____ ______ ___________ ______
|
||||||
|
__ /_______ ____ /_ ___ /___(_)___ /_
|
||||||
|
_ __/_ __ `/__ __ \__ / __ / __ __ \
|
||||||
|
/ /_ / /_/ / _ /_/ /_ / _ / _ /_/ /
|
||||||
|
\__/ \__,_/ /_.___/ /_/ /_/ /_.___/
|
||||||
|
|
||||||
|
|
||||||
|
Tablib is a format-agnostic tabular dataset library, written in Python.
|
||||||
|
|
||||||
|
Output formats supported:
|
||||||
|
|
||||||
|
- Excel (Sets + Books)
|
||||||
|
- JSON (Sets + Books)
|
||||||
|
- YAML (Sets + Books)
|
||||||
|
- Pandas DataFrames (Sets)
|
||||||
|
- HTML (Sets)
|
||||||
|
- Jira (Sets)
|
||||||
|
- TSV (Sets)
|
||||||
|
- ODS (Sets)
|
||||||
|
- CSV (Sets)
|
||||||
|
- DBF (Sets)
|
||||||
|
|
||||||
|
Note that tablib *purposefully* excludes XML support. It always will. (Note: This is a
|
||||||
|
joke. Pull requests are welcome.)
|
||||||
|
|
||||||
|
Tablib documentation is graciously hosted on https://tablib.readthedocs.io
|
||||||
|
|
||||||
|
It is also available in the ``docs`` directory of the source distribution.
|
||||||
|
|
||||||
|
Make sure to check out [Tablib on PyPI](https://pypi.org/project/tablib/)!
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
Please see the [contributing guide](https://github.com/jazzband/tablib/blob/master/.github/CONTRIBUTING.md).
|
||||||
-188
@@ -1,188 +0,0 @@
|
|||||||
Tablib: format-agnostic tabular dataset library
|
|
||||||
===============================================
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
_____ ______ ___________ ______
|
|
||||||
__ /_______ ____ /_ ___ /___(_)___ /_
|
|
||||||
_ __/_ __ `/__ __ \__ / __ / __ __ \
|
|
||||||
/ /_ / /_/ / _ /_/ /_ / _ / _ /_/ /
|
|
||||||
\__/ \__,_/ /_.___/ /_/ /_/ /_.___/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Tablib is a format-agnostic tabular dataset library, written in Python.
|
|
||||||
|
|
||||||
Output formats supported:
|
|
||||||
|
|
||||||
- Excel (Sets + Books)
|
|
||||||
- JSON (Sets + Books)
|
|
||||||
- YAML (Sets + Books)
|
|
||||||
- CSV (Sets)
|
|
||||||
|
|
||||||
Import formats supported:
|
|
||||||
|
|
||||||
- JSON (Sets + Books)
|
|
||||||
- YAML (Sets + Books)
|
|
||||||
- CSV (Sets)
|
|
||||||
|
|
||||||
Note that tablib *purposefully* excludes XML support. It always will.
|
|
||||||
|
|
||||||
Overview
|
|
||||||
--------
|
|
||||||
|
|
||||||
`tablib.Dataset()`
|
|
||||||
A Dataset is a table of tabular data. It may or may not have a header row. They can be build and maniuplated as raw Python datatypes (Lists of tuples|dictonaries). Datasets can be imported from JSON, YAML, and CSV; they can be exported to Excel (XLS), JSON, YAML, and CSV.
|
|
||||||
|
|
||||||
`tablib.Databook()`
|
|
||||||
A Databook is a set of Datasets. The most common form of a Databook is an Excel file with multiple spreadsheets. Databooks can be imported from JSON and YAML; they can be exported to Excel (XLS), JSON, and YAML.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
|
|
||||||
Populate fresh data files: ::
|
|
||||||
|
|
||||||
headers = ('first_name', 'last_name')
|
|
||||||
|
|
||||||
data = [
|
|
||||||
('John', 'Adams'),
|
|
||||||
('George', 'Washington')
|
|
||||||
]
|
|
||||||
|
|
||||||
data = tablib.Dataset(*data, headers=headers)
|
|
||||||
|
|
||||||
|
|
||||||
Intelligently add new rows: ::
|
|
||||||
|
|
||||||
>>> data.append(('Henry', 'Ford'))
|
|
||||||
|
|
||||||
Intelligently add new columns: ::
|
|
||||||
|
|
||||||
>>> data.append(col=('age', 90, 67, 83))
|
|
||||||
|
|
||||||
Slice rows: ::
|
|
||||||
|
|
||||||
>>> print data[:2]
|
|
||||||
[('John', 'Adams', 90), ('George', 'Washington', 67)]
|
|
||||||
|
|
||||||
|
|
||||||
Slice columns by header: ::
|
|
||||||
|
|
||||||
>>> print data['first_name']
|
|
||||||
['John', 'George', 'Henry']
|
|
||||||
|
|
||||||
Easily delete rows: ::
|
|
||||||
|
|
||||||
>>> del data[1]
|
|
||||||
|
|
||||||
Exports
|
|
||||||
-------
|
|
||||||
|
|
||||||
Drumroll please...........
|
|
||||||
|
|
||||||
JSON!
|
|
||||||
+++++
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> print data.json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"last_name": "Adams",
|
|
||||||
"age": 90,
|
|
||||||
"first_name": "John"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"last_name": "Ford",
|
|
||||||
"age": 83,
|
|
||||||
"first_name": "Henry"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
YAML!
|
|
||||||
+++++
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> print data.yaml
|
|
||||||
- {age: 90, first_name: John, last_name: Adams}
|
|
||||||
- {age: 83, first_name: Henry, last_name: Ford}
|
|
||||||
|
|
||||||
CSV...
|
|
||||||
++++++
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> print data.csv
|
|
||||||
first_name,last_name,age
|
|
||||||
John,Adams,90
|
|
||||||
Henry,Ford,83
|
|
||||||
|
|
||||||
EXCEL!
|
|
||||||
++++++
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> open('people.xls', 'wb').write(data.xls)
|
|
||||||
|
|
||||||
It's that easy.
|
|
||||||
|
|
||||||
Imports!
|
|
||||||
--------
|
|
||||||
|
|
||||||
JSON
|
|
||||||
++++
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> data.json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
|
|
||||||
>>> print data[0]
|
|
||||||
('John', 'Adams', 90)
|
|
||||||
|
|
||||||
|
|
||||||
YAML
|
|
||||||
++++
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> data.yaml = '- {age: 90, first_name: John, last_name: Adams}'
|
|
||||||
>>> print data[0]
|
|
||||||
('John', 'Adams', 90)
|
|
||||||
|
|
||||||
CSV
|
|
||||||
+++
|
|
||||||
::
|
|
||||||
|
|
||||||
>>> data.yaml = 'age, first_name, last_name\n90, John, Adams'
|
|
||||||
>>> print data[0]
|
|
||||||
('John', 'Adams', 90)
|
|
||||||
|
|
||||||
>>> print data.yaml
|
|
||||||
- {age: 90, first_name: John, last_name: Adams}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
|
|
||||||
To install tablib, simply: ::
|
|
||||||
|
|
||||||
$ pip install tablib
|
|
||||||
|
|
||||||
Or, if you absolutely must: ::
|
|
||||||
|
|
||||||
$ easy_install tablib
|
|
||||||
|
|
||||||
|
|
||||||
Contribute
|
|
||||||
----------
|
|
||||||
|
|
||||||
If you'd like to contribute, simply fork `the repository`_, commit your changes to the **develop** branch (or branch off of it), and send a pull request. Make sure you add yourself to AUTHORS_.
|
|
||||||
|
|
||||||
|
|
||||||
Roadmap
|
|
||||||
-------
|
|
||||||
- Release CLI Interface
|
|
||||||
- Auto-detect import format
|
|
||||||
- Add possible other exports (SQL?)
|
|
||||||
- Ability to assign types to rows (set, regex=, &c.)
|
|
||||||
|
|
||||||
.. _`the repository`: http://github.com/kennethreitz/tablib
|
|
||||||
.. _AUTHORS: http://github.com/kennethreitz/tablib/blob/master/AUTHORS
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Release checklist
|
||||||
|
|
||||||
|
Jazzband guidelines: https://jazzband.co/about/releases
|
||||||
|
|
||||||
|
* [ ] Get master to the appropriate code release state.
|
||||||
|
[GitHub Actions](https://github.com/jazzband/tablib/actions)
|
||||||
|
should pass on master.
|
||||||
|
[](https://github.com/jazzband/tablib/actions)
|
||||||
|
|
||||||
|
* [ ] Check [HISTORY.md](https://github.com/jazzband/tablib/blob/master/HISTORY.md),
|
||||||
|
update version number and release date
|
||||||
|
|
||||||
|
* [ ] Tag with version number and push tag, for example:
|
||||||
|
```bash
|
||||||
|
git tag -a v0.14.0 -m v0.14.0
|
||||||
|
git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
* [ ] Once GitHub Actions has built and uploaded distributions, check files at
|
||||||
|
[Jazzband](https://jazzband.co/projects/tablib) and release to
|
||||||
|
[PyPI](https://pypi.org/pypi/tablib)
|
||||||
|
|
||||||
|
* [ ] Check installation:
|
||||||
|
```bash
|
||||||
|
pip uninstall -y tablib && pip install -U tablib
|
||||||
|
```
|
||||||
|
|
||||||
|
* [ ] Create new GitHub release: https://github.com/jazzband/tablib/releases/new
|
||||||
|
* Tag: Pick existing tag "v0.14.0"
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tablib.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tablib.qhc"
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/Tablib"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tablib"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
make -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<h3><a href="https://tablib.readthedocs.io">About Tablib</a></h3>
|
||||||
|
<p>
|
||||||
|
Tablib is an MIT Licensed format-agnostic tabular dataset library, written in Python. It allows you to import, export, and manipulate tabular data sets. Advanced features include, segregation, dynamic columns, tags & filtering, and seamless format import & export.
|
||||||
|
</p>
|
||||||
|
<h3>Useful Links</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://tablib.readthedocs.io">The Tablib Website</a></li>
|
||||||
|
<li><a href="https://pypi.org/project/tablib">Tablib @ PyPI</a></li>
|
||||||
|
<li><a href="https://github.com/jazzband/tablib">Tablib @ GitHub</a></li>
|
||||||
|
<li><a href="https://github.com/jazzband/tablib/issues">Issue Tracker</a></li>
|
||||||
|
<li><a href="https://github.com/jazzband/tablib/blob/master/HISTORY.md">Changelog</a></li>
|
||||||
|
</ul>
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<h3><a href="https://tablib.readthedocs.io">About Tablib</a></h3>
|
||||||
|
<p>
|
||||||
|
Tablib is an MIT Licensed format-agnostic tabular dataset library, written in Python. It allows you to import, export, and manipulate tabular data sets. Advanced features include, segregation, dynamic columns, tags & filtering, and seamless format import & export.
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.. _api:
|
||||||
|
|
||||||
|
===
|
||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
|
||||||
|
.. module:: tablib
|
||||||
|
|
||||||
|
This part of the documentation covers all the interfaces of Tablib. For
|
||||||
|
parts where Tablib depends on external libraries, we document the most
|
||||||
|
important right here and provide links to the canonical documentation.
|
||||||
|
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Dataset Object
|
||||||
|
--------------
|
||||||
|
|
||||||
|
|
||||||
|
.. autoclass:: Dataset
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
|
||||||
|
---------------
|
||||||
|
Databook Object
|
||||||
|
---------------
|
||||||
|
|
||||||
|
|
||||||
|
.. autoclass:: Databook
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---------
|
||||||
|
Functions
|
||||||
|
---------
|
||||||
|
|
||||||
|
|
||||||
|
.. autofunction:: detect_format
|
||||||
|
|
||||||
|
.. autofunction:: import_set
|
||||||
|
|
||||||
|
|
||||||
|
----------
|
||||||
|
Exceptions
|
||||||
|
----------
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: InvalidDatasetType
|
||||||
|
|
||||||
|
You're trying to add something that doesn't quite look right.
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: InvalidDimensions
|
||||||
|
|
||||||
|
You're trying to add something that doesn't quite fit right.
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: UnsupportedFormat
|
||||||
|
|
||||||
|
You're trying to add something that doesn't quite taste right.
|
||||||
|
|
||||||
|
|
||||||
|
Now, go start some :ref:`Tablib Development <development>`.
|
||||||
+230
@@ -0,0 +1,230 @@
|
|||||||
|
#
|
||||||
|
# Tablib documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Tue Oct 5 15:25:21 2010.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
import tablib
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
# sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage',
|
||||||
|
'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'
|
||||||
|
]
|
||||||
|
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'Tablib'
|
||||||
|
copyright = '2019 Jazzband'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = tablib.__version__
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '.'.join(tablib.__version__.split('.')[:2])
|
||||||
|
# for example take major/minor
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
# add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
# pygments_style = ''
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
# html_static_path = ['static']
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
html_sidebars = {
|
||||||
|
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
|
||||||
|
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
|
||||||
|
'sourcelink.html', 'searchbox.html']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
html_show_sphinx = False
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'Tablibdoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
# The paper size ('letter' or 'a4').
|
||||||
|
#latex_paper_size = 'letter'
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#latex_font_size = '10pt'
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'Tablib.tex', 'Tablib Documentation',
|
||||||
|
'Jazzband', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
latex_use_modindex = False
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
'papersize': 'a4paper',
|
||||||
|
'pointsize': '12pt',
|
||||||
|
}
|
||||||
|
latex_use_parts = True
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#latex_preamble = ''
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output --------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('index', 'tablib', 'Tablib Documentation',
|
||||||
|
['Jazzband'], 1)
|
||||||
|
]
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
.. _development:
|
||||||
|
|
||||||
|
Development
|
||||||
|
===========
|
||||||
|
|
||||||
|
Tablib is under active development, and contributors are welcome.
|
||||||
|
|
||||||
|
If you have a feature request, suggestion, or bug report, please open a new
|
||||||
|
issue on GitHub_. To submit patches, please send a pull request on GitHub_.
|
||||||
|
|
||||||
|
.. _GitHub: https://github.com/jazzband/tablib/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. _design:
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
Design Considerations
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Tablib was developed with a few :pep:`20` idioms in mind.
|
||||||
|
|
||||||
|
#. Beautiful is better than ugly.
|
||||||
|
#. Explicit is better than implicit.
|
||||||
|
#. Simple is better than complex.
|
||||||
|
#. Complex is better than complicated.
|
||||||
|
#. Readability counts.
|
||||||
|
|
||||||
|
A few other things to keep in mind:
|
||||||
|
|
||||||
|
#. Keep your code DRY.
|
||||||
|
#. Strive to be as simple (to use) as possible.
|
||||||
|
|
||||||
|
.. _scm:
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Source Control
|
||||||
|
--------------
|
||||||
|
|
||||||
|
|
||||||
|
Tablib source is controlled with Git_, the lean, mean, distributed source
|
||||||
|
control machine.
|
||||||
|
|
||||||
|
The repository is publicly accessible.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
git clone git://github.com/jazzband/tablib.git
|
||||||
|
|
||||||
|
The project is hosted on **GitHub**.
|
||||||
|
|
||||||
|
GitHub:
|
||||||
|
https://github.com/jazzband/tablib
|
||||||
|
|
||||||
|
|
||||||
|
Git Branch Structure
|
||||||
|
++++++++++++++++++++
|
||||||
|
|
||||||
|
Feature / Hotfix / Release branches follow a `Successful Git Branching Model`_ .
|
||||||
|
Git-flow_ is a great tool for managing the repository. I highly recommend it.
|
||||||
|
|
||||||
|
``master``
|
||||||
|
Current production release (|version|) on PyPi.
|
||||||
|
|
||||||
|
Each release is tagged.
|
||||||
|
|
||||||
|
When submitting patches, please place your feature/change in its own branch prior to opening a pull request on GitHub_.
|
||||||
|
|
||||||
|
|
||||||
|
.. _Git: https://git-scm.org
|
||||||
|
.. _`Successful Git Branching Model`: https://nvie.com/posts/a-successful-git-branching-model/
|
||||||
|
.. _git-flow: https://github.com/nvie/gitflow
|
||||||
|
|
||||||
|
|
||||||
|
.. _newformats:
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Adding New Formats
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Tablib welcomes new format additions! Format suggestions include:
|
||||||
|
|
||||||
|
* MySQL Dump
|
||||||
|
|
||||||
|
|
||||||
|
Coding by Convention
|
||||||
|
++++++++++++++++++++
|
||||||
|
|
||||||
|
Tablib features a micro-framework for adding format support.
|
||||||
|
The easiest way to understand it is to use it.
|
||||||
|
So, let's define our own format, named *xxx*.
|
||||||
|
|
||||||
|
From version 1.0, Tablib formats are class-based and can be dynamically
|
||||||
|
registered.
|
||||||
|
|
||||||
|
1. Write your custom format class::
|
||||||
|
|
||||||
|
class MyXXXFormatClass:
|
||||||
|
title = 'xxx'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dset):
|
||||||
|
....
|
||||||
|
# returns string representation of given dataset
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, dbook):
|
||||||
|
....
|
||||||
|
# returns string representation of given databook
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream):
|
||||||
|
...
|
||||||
|
# populates given Dataset with given datastream
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_book(cls, dbook, in_stream):
|
||||||
|
...
|
||||||
|
# returns Databook instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
...
|
||||||
|
# returns True if given stream is parsable as xxx
|
||||||
|
|
||||||
|
.. admonition:: Excluding Support
|
||||||
|
|
||||||
|
If the format excludes support for an import/export mechanism (*e.g.*
|
||||||
|
:class:`csv <tablib.Dataset.csv>` excludes
|
||||||
|
:class:`Databook <tablib.Databook>` support),
|
||||||
|
simply don't define the respective class methods.
|
||||||
|
Appropriate errors will be raised.
|
||||||
|
|
||||||
|
2. Register your class::
|
||||||
|
|
||||||
|
from tablib.formats import registry
|
||||||
|
|
||||||
|
registry.register('xxx', MyXXXFormatClass())
|
||||||
|
|
||||||
|
3. From then on, you should be able to use your new custom format as if it were
|
||||||
|
a built-in Tablib format, e.g. using ``dataset.export('xxx')`` will use the
|
||||||
|
``MyXXXFormatClass.export_set`` method.
|
||||||
|
|
||||||
|
.. _testing:
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Testing Tablib
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Testing is crucial to Tablib's stability.
|
||||||
|
This stable project is used in production by many companies and developers,
|
||||||
|
so it is important to be certain that every version released is fully operational.
|
||||||
|
When developing a new feature for Tablib, be sure to write proper tests for it as well.
|
||||||
|
|
||||||
|
When developing a feature for Tablib,
|
||||||
|
the easiest way to test your changes for potential issues is to simply run the test suite directly.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ tox
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Continuous Integration
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Every pull request is automatically tested and inspected upon receipt with `GitHub Actions`_.
|
||||||
|
If you broke the build, you will receive an email accordingly.
|
||||||
|
|
||||||
|
Anyone may view the build status and history at any time.
|
||||||
|
|
||||||
|
https://github.com/jazzband/tablib/actions
|
||||||
|
|
||||||
|
Additional reports will also be included here in the future, including :pep:`8` checks and stress reports for extremely large datasets.
|
||||||
|
|
||||||
|
.. _`GitHub Actions`: https://github.com/jazzband/tablib/actions
|
||||||
|
|
||||||
|
|
||||||
|
.. _docs:
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
Building the Docs
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Documentation is written in the powerful, flexible,
|
||||||
|
and standard Python documentation format, `reStructured Text`_.
|
||||||
|
Documentation builds are powered by the powerful Pocoo project, Sphinx_.
|
||||||
|
The :ref:`API Documentation <api>` is mostly documented inline throughout the module.
|
||||||
|
|
||||||
|
The Docs live in ``tablib/docs``.
|
||||||
|
In order to build them, you will first need to install Sphinx.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install sphinx
|
||||||
|
|
||||||
|
|
||||||
|
Then, to build an HTML version of the docs, simply run the following from the ``docs`` directory:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ make html
|
||||||
|
|
||||||
|
Your ``docs/_build/html`` directory will then contain an HTML representation of the documentation,
|
||||||
|
ready for publication on most web servers.
|
||||||
|
|
||||||
|
You can also generate the documentation in **epub**, **latex**, **json**, *&c* similarly.
|
||||||
|
|
||||||
|
.. _`reStructured Text`: http://docutils.sourceforge.net/rst.html
|
||||||
|
.. _Sphinx: http://sphinx.pocoo.org
|
||||||
|
.. _`GitHub Pages`: https://pages.github.com
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
Make sure to check out the :ref:`API Documentation <api>`.
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
.. _formats:
|
||||||
|
|
||||||
|
=======
|
||||||
|
Formats
|
||||||
|
=======
|
||||||
|
|
||||||
|
Tablib supports a wide variety of different tabular formats, both for input and
|
||||||
|
output. Moreover, you can :ref:`register your own formats <newformats>`.
|
||||||
|
|
||||||
|
cli
|
||||||
|
===
|
||||||
|
|
||||||
|
The ``cli`` format is currently export-only. The exports produce a representation
|
||||||
|
table suited to a terminal.
|
||||||
|
|
||||||
|
When exporting to a CLI you can pass the table format with the ``tablefmt``
|
||||||
|
parameter, the supported formats are::
|
||||||
|
|
||||||
|
>>> import tabulate
|
||||||
|
>>> list(tabulate._table_formats)
|
||||||
|
['simple', 'plain', 'grid', 'fancy_grid', 'github', 'pipe', 'orgtbl',
|
||||||
|
'jira', 'presto', 'psql', 'rst', 'mediawiki', 'moinmoin', 'youtrack',
|
||||||
|
'html', 'latex', 'latex_raw', 'latex_booktabs', 'tsv', 'textile']
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
dataset.export("cli", tablefmt="github")
|
||||||
|
dataset.export("cli", tablefmt="grid")
|
||||||
|
|
||||||
|
This format is optional, install Tablib with ``pip install "tablib[cli]"`` to
|
||||||
|
make the format available.
|
||||||
|
|
||||||
|
csv
|
||||||
|
===
|
||||||
|
|
||||||
|
When you import CSV data, you can specify if the first line of your data source
|
||||||
|
is headers with the ``headers`` boolean parameter (defaults to ``True``)::
|
||||||
|
|
||||||
|
import tablib
|
||||||
|
|
||||||
|
tablib.import_set(your_data_stream, format='csv', headers=False)
|
||||||
|
|
||||||
|
When exporting with the ``csv`` format, the top row will contain headers, if
|
||||||
|
they have been set. Otherwise, the top row will contain the first row of the
|
||||||
|
dataset.
|
||||||
|
|
||||||
|
When importing a CSV data source or exporting a dataset as CSV, you can pass any
|
||||||
|
parameter supported by the :py:func:`csv.reader` and :py:func:`csv.writer`
|
||||||
|
functions. For example::
|
||||||
|
|
||||||
|
tablib.import_set(your_data_stream, format='csv', dialect='unix')
|
||||||
|
|
||||||
|
dataset.export('csv', delimiter=' ', quotechar='|')
|
||||||
|
|
||||||
|
.. admonition:: Line endings
|
||||||
|
|
||||||
|
Exporting uses \\r\\n line endings by default so, make sure to include
|
||||||
|
``newline=''`` otherwise you will get a blank line between each row
|
||||||
|
when you open the file in Excel::
|
||||||
|
|
||||||
|
with open('output.csv', 'w', newline='') as f:
|
||||||
|
f.write(dataset.export('csv'))
|
||||||
|
|
||||||
|
If you do not do this, and you export the file on Windows, your
|
||||||
|
CSV file will open in Excel with a blank line between each row.
|
||||||
|
|
||||||
|
dbf
|
||||||
|
===
|
||||||
|
|
||||||
|
Import/export using the dBASE_ format.
|
||||||
|
|
||||||
|
.. admonition:: Binary Warning
|
||||||
|
|
||||||
|
The ``dbf`` format contains binary data, so make sure to write in binary
|
||||||
|
mode::
|
||||||
|
|
||||||
|
with open('output.dbf', 'wb') as f:
|
||||||
|
f.write(dataset.export('dbf')
|
||||||
|
|
||||||
|
.. _dBASE: https://en.wikipedia.org/wiki/DBase
|
||||||
|
|
||||||
|
df (DataFrame)
|
||||||
|
==============
|
||||||
|
|
||||||
|
Import/export using the pandas_ DataFrame format. This format is optional,
|
||||||
|
install Tablib with ``pip install "tablib[pandas]"`` to make the format available.
|
||||||
|
|
||||||
|
.. _pandas: https://pandas.pydata.org/
|
||||||
|
|
||||||
|
html
|
||||||
|
====
|
||||||
|
|
||||||
|
The ``html`` format is currently export-only. The exports produce an HTML page
|
||||||
|
with the data in a ``<table>``. If headers have been set, they will be used as
|
||||||
|
table headers.
|
||||||
|
|
||||||
|
This format is optional, install Tablib with ``pip install "tablib[html]"`` to
|
||||||
|
make the format available.
|
||||||
|
|
||||||
|
jira
|
||||||
|
====
|
||||||
|
|
||||||
|
The ``jira`` format is currently export-only. Exports format the dataset
|
||||||
|
according to the Jira table syntax::
|
||||||
|
|
||||||
|
||heading 1||heading 2||heading 3||
|
||||||
|
|col A1|col A2|col A3|
|
||||||
|
|col B1|col B2|col B3|
|
||||||
|
|
||||||
|
json
|
||||||
|
====
|
||||||
|
|
||||||
|
Import/export using the JSON_ format. If headers have been set, a JSON list of
|
||||||
|
objects will be returned. If no headers have been set, a JSON list of lists
|
||||||
|
(rows) will be returned instead.
|
||||||
|
|
||||||
|
Import assumes (for now) that headers exist.
|
||||||
|
|
||||||
|
.. _JSON: http://json.org/
|
||||||
|
|
||||||
|
latex
|
||||||
|
=====
|
||||||
|
|
||||||
|
Import/export using the LaTeX_ format. This format is export-only.
|
||||||
|
If a title has been set, it will be exported as the table caption.
|
||||||
|
|
||||||
|
.. _LaTeX: https://www.latex-project.org/
|
||||||
|
|
||||||
|
ods
|
||||||
|
===
|
||||||
|
|
||||||
|
Export data in OpenDocument Spreadsheet format. The ``ods`` format is currently
|
||||||
|
export-only.
|
||||||
|
|
||||||
|
This format is optional, install Tablib with ``pip install "tablib[ods]"`` to
|
||||||
|
make the format available.
|
||||||
|
|
||||||
|
.. admonition:: Binary Warning
|
||||||
|
|
||||||
|
:class:`Dataset.ods` contains binary data, so make sure to write in binary mode::
|
||||||
|
|
||||||
|
with open('output.ods', 'wb') as f:
|
||||||
|
f.write(data.ods)
|
||||||
|
|
||||||
|
rst
|
||||||
|
===
|
||||||
|
|
||||||
|
Export data as a reStructuredText_ table representation of a dataset. The
|
||||||
|
``rst`` format is export-only.
|
||||||
|
|
||||||
|
Exporting returns a simple table if the text in the first column is never
|
||||||
|
wrapped, otherwise returns a grid table::
|
||||||
|
|
||||||
|
>>> from tablib import Dataset
|
||||||
|
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||||
|
>>> data = Dataset()
|
||||||
|
>>> data.headers = ['A', 'B', 'A and B']
|
||||||
|
>>> for a, b in bits:
|
||||||
|
... data.append([bool(a), bool(b), bool(a * b)])
|
||||||
|
>>> table = data.export('rst')
|
||||||
|
>>> table.split('\\n') == [
|
||||||
|
... '===== ===== =====',
|
||||||
|
... ' A B A and',
|
||||||
|
... ' B ',
|
||||||
|
... '===== ===== =====',
|
||||||
|
... 'False False False',
|
||||||
|
... 'True False False',
|
||||||
|
... 'False True False',
|
||||||
|
... 'True True True ',
|
||||||
|
... '===== ===== =====',
|
||||||
|
... ]
|
||||||
|
True
|
||||||
|
|
||||||
|
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
|
||||||
|
|
||||||
|
tsv
|
||||||
|
===
|
||||||
|
|
||||||
|
A variant of the csv_ format with tabulators as fields separators.
|
||||||
|
|
||||||
|
xls
|
||||||
|
===
|
||||||
|
|
||||||
|
Import/export data in Legacy Excel Spreadsheet representation.
|
||||||
|
|
||||||
|
This format is optional, install Tablib with ``pip install "tablib[xls]"`` to
|
||||||
|
make the format available.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
XLS files are limited to a maximum of 65,000 rows. Use xlsx_ to avoid this
|
||||||
|
limitation.
|
||||||
|
|
||||||
|
.. admonition:: Binary Warning
|
||||||
|
|
||||||
|
The ``xls`` file format is binary, so make sure to write in binary mode::
|
||||||
|
|
||||||
|
with open('output.xls', 'wb') as f:
|
||||||
|
f.write(data.export('xls'))
|
||||||
|
|
||||||
|
xlsx
|
||||||
|
====
|
||||||
|
|
||||||
|
Import/export data in Excel 07+ Spreadsheet representation.
|
||||||
|
|
||||||
|
This format is optional, install Tablib with ``pip install "tablib[xlsx]"`` to
|
||||||
|
make the format available.
|
||||||
|
|
||||||
|
The ``import_set()`` and ``import_book()`` methods accept keyword
|
||||||
|
argument ``read_only``. If its value is ``True`` (the default), the
|
||||||
|
XLSX data source is read lazily. Lazy reading generally reduces time
|
||||||
|
and memory consumption, especially for large spreadsheets. However,
|
||||||
|
it relies on the XLSX data source declaring correct dimensions. Some
|
||||||
|
programs generate XLSX files with incorrect dimensions. Such files
|
||||||
|
may need to be loaded with this optimization turned off by passing
|
||||||
|
``read_only=False``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When reading an ``xlsx`` file containing formulas in its cells, Tablib will
|
||||||
|
read the cell values, not the cell formulas.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.0
|
||||||
|
|
||||||
|
Reads cell values instead of formulas.
|
||||||
|
|
||||||
|
.. admonition:: Binary Warning
|
||||||
|
|
||||||
|
The ``xlsx`` file format is binary, so make sure to write in binary mode::
|
||||||
|
|
||||||
|
with open('output.xlsx', 'wb') as f:
|
||||||
|
f.write(data.export('xlsx'))
|
||||||
|
|
||||||
|
yaml
|
||||||
|
====
|
||||||
|
|
||||||
|
Import/export data in the YAML_ format.
|
||||||
|
When exporting, if headers have been set, a YAML list of objects will be
|
||||||
|
returned. If no headers have been set, a YAML list of lists (rows) will be
|
||||||
|
returned instead.
|
||||||
|
|
||||||
|
Import assumes (for now) that headers exist.
|
||||||
|
|
||||||
|
This format is optional, install Tablib with ``pip install "tablib[yaml]"`` to
|
||||||
|
make the format available.
|
||||||
|
|
||||||
|
.. _YAML: https://yaml.org
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
.. Tablib documentation master file, created by
|
||||||
|
sphinx-quickstart on Tue Oct 5 15:25:21 2010.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root ``toctree`` directive.
|
||||||
|
|
||||||
|
Tablib: Pythonic Tabular Datasets
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Release v\ |version|. (:ref:`Installation <install>`)
|
||||||
|
|
||||||
|
.. Contents:
|
||||||
|
..
|
||||||
|
.. .. toctree::
|
||||||
|
.. :maxdepth: 2
|
||||||
|
..
|
||||||
|
|
||||||
|
.. Indices and tables
|
||||||
|
.. ==================
|
||||||
|
..
|
||||||
|
.. * :ref:`genindex`
|
||||||
|
.. * :ref:`modindex`
|
||||||
|
.. * :ref:`search`
|
||||||
|
|
||||||
|
|
||||||
|
Tablib is an `MIT Licensed <https://mit-license.org/>`_ format-agnostic tabular dataset library, written in Python.
|
||||||
|
It allows you to import, export, and manipulate tabular data sets.
|
||||||
|
Advanced features include segregation, dynamic columns, tags & filtering,
|
||||||
|
and seamless format import & export.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> data = tablib.Dataset(headers=['First Name', 'Last Name', 'Age'])
|
||||||
|
>>> for i in [('Kenneth', 'Reitz', 22), ('Bessie', 'Monke', 21)]:
|
||||||
|
... data.append(i)
|
||||||
|
|
||||||
|
|
||||||
|
>>> print(data.export('json'))
|
||||||
|
[{"Last Name": "Reitz", "First Name": "Kenneth", "Age": 22}, {"Last Name": "Monke", "First Name": "Bessie", "Age": 21}]
|
||||||
|
|
||||||
|
>>> print(data.export('yaml'))
|
||||||
|
- {Age: 22, First Name: Kenneth, Last Name: Reitz}
|
||||||
|
- {Age: 21, First Name: Bessie, Last Name: Monke}
|
||||||
|
|
||||||
|
>>> data.export('xlsx')
|
||||||
|
<redacted binary data>
|
||||||
|
|
||||||
|
>>> data.export('df')
|
||||||
|
First Name Last Name Age
|
||||||
|
0 Kenneth Reitz 22
|
||||||
|
1 Bessie Monke 21
|
||||||
|
|
||||||
|
|
||||||
|
Testimonials
|
||||||
|
------------
|
||||||
|
|
||||||
|
`National Geographic <https://www.nationalgeographic.com/>`_,
|
||||||
|
`Digg, Inc <https://digg.com/>`_,
|
||||||
|
`Northrop Grumman <https://www.northropgrumman.com/>`_,
|
||||||
|
`Discovery Channel <https://dsc.discovery.com/>`_,
|
||||||
|
and `The Sunlight Foundation <https://sunlightfoundation.com/>`_ use Tablib internally.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Greg Thorton**
|
||||||
|
Tablib by @kennethreitz saved my life.
|
||||||
|
I had to consolidate like 5 huge poorly maintained lists of domains and data.
|
||||||
|
It was a breeze!
|
||||||
|
|
||||||
|
**Dave Coutts**
|
||||||
|
It's turning into one of my most used modules of 2010.
|
||||||
|
You really hit a sweet spot for managing tabular data with a minimal amount of code and effort.
|
||||||
|
|
||||||
|
**Joshua Ourisman**
|
||||||
|
Tablib has made it so much easier to deal with the inevitable 'I want an Excel file!' requests from clients...
|
||||||
|
|
||||||
|
**Brad Montgomery**
|
||||||
|
I think you nailed the "Python Zen" with tablib.
|
||||||
|
Thanks again for an awesome lib!
|
||||||
|
|
||||||
|
|
||||||
|
User's Guide
|
||||||
|
------------
|
||||||
|
|
||||||
|
This part of the documentation, which is mostly prose, begins with some background information about Tablib, then focuses on step-by-step instructions for getting the most out of your datasets.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
intro
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
install
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
tutorial
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
formats
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
development
|
||||||
|
|
||||||
|
|
||||||
|
API Reference
|
||||||
|
-------------
|
||||||
|
|
||||||
|
If you are looking for information on a specific function, class or
|
||||||
|
method, this part of the documentation is for you.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
api
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
.. _install:
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
This part of the documentation covers the installation of Tablib. The first step to using any software package is getting it properly installed.
|
||||||
|
|
||||||
|
|
||||||
|
.. _installing:
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
Installing Tablib
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Distribute & Pip
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Of course, the recommended way to install Tablib is with `pip <https://pip.pypa.io>`_:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install tablib
|
||||||
|
|
||||||
|
You can also choose to install more dependencies to have more import/export
|
||||||
|
formats available:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install "tablib[xlsx]"
|
||||||
|
|
||||||
|
Or all possible formats:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install "tablib[all]"
|
||||||
|
|
||||||
|
which is equivalent to:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install "tablib[html, pandas, ods, xls, xlsx, yaml]"
|
||||||
|
|
||||||
|
-------------------
|
||||||
|
Download the Source
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
You can also install Tablib from source.
|
||||||
|
The latest release (|version|) is available from GitHub.
|
||||||
|
|
||||||
|
* tarball_
|
||||||
|
* zipball_
|
||||||
|
|
||||||
|
.. _
|
||||||
|
|
||||||
|
Once you have a copy of the source,
|
||||||
|
you can embed it in your Python package,
|
||||||
|
or install it into your site-packages easily.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ python setup.py install
|
||||||
|
|
||||||
|
|
||||||
|
To download the full source history from Git, see :ref:`Source Control <scm>`.
|
||||||
|
|
||||||
|
.. _tarball: https://github.com/jazzband/tablib/tarball/master
|
||||||
|
.. _zipball: https://github.com/jazzband/tablib/zipball/master
|
||||||
|
|
||||||
|
|
||||||
|
.. _updates:
|
||||||
|
|
||||||
|
Staying Updated
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The latest version of Tablib will always be available here:
|
||||||
|
|
||||||
|
* PyPI: https://pypi.org/project/tablib/
|
||||||
|
* GitHub: https://github.com/jazzband/tablib/
|
||||||
|
|
||||||
|
When a new version is available, upgrading is simple::
|
||||||
|
|
||||||
|
$ pip install tablib --upgrade
|
||||||
|
|
||||||
|
|
||||||
|
Now, go get a :ref:`Quick Start <quickstart>`.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
.. _intro:
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
This part of the documentation covers all the interfaces of Tablib.
|
||||||
|
Tablib is a format-agnostic tabular dataset library, written in Python.
|
||||||
|
It allows you to Pythonically import, export, and manipulate tabular data sets.
|
||||||
|
Advanced features include segregation, dynamic columns, tags/filtering, and
|
||||||
|
seamless format import/export.
|
||||||
|
|
||||||
|
|
||||||
|
Philosophy
|
||||||
|
----------
|
||||||
|
|
||||||
|
Tablib was developed with a few :pep:`20` idioms in mind.
|
||||||
|
|
||||||
|
#. Beautiful is better than ugly.
|
||||||
|
#. Explicit is better than implicit.
|
||||||
|
#. Simple is better than complex.
|
||||||
|
#. Complex is better than complicated.
|
||||||
|
#. Readability counts.
|
||||||
|
|
||||||
|
All contributions to Tablib should keep these important rules in mind.
|
||||||
|
|
||||||
|
.. _license:
|
||||||
|
|
||||||
|
Tablib License
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Tablib is released under terms of `The MIT License`_.
|
||||||
|
|
||||||
|
Copyright 2017 Kenneth Reitz
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
.. _`The MIT License`: https://opensource.org/licenses/mit-license.php
|
||||||
|
|
||||||
|
.. _pythonsupport:
|
||||||
|
|
||||||
|
Pythons Supported
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Python 3.6+ is officially supported.
|
||||||
|
|
||||||
|
Now, go :ref:`install Tablib <install>`.
|
||||||
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
\definecolor{TitleColor}{rgb}{0,0,0}
|
||||||
|
\definecolor{InnerLinkColor}{rgb}{0,0,0}
|
||||||
|
|
||||||
|
\renewcommand{\maketitle}{%
|
||||||
|
\begin{titlepage}%
|
||||||
|
\let\footnotesize\small
|
||||||
|
\let\footnoterule\relax
|
||||||
|
\ifsphinxpdfoutput
|
||||||
|
\begingroup
|
||||||
|
% This \def is required to deal with multi-line authors; it
|
||||||
|
% changes \\ to ', ' (comma-space), making it pass muster for
|
||||||
|
% generating document info in the PDF file.
|
||||||
|
\def\\{, }
|
||||||
|
\pdfinfo{
|
||||||
|
/Author (\@author)
|
||||||
|
/Title (\@title)
|
||||||
|
}
|
||||||
|
\endgroup
|
||||||
|
\fi
|
||||||
|
\begin{flushright}%
|
||||||
|
%\sphinxlogo%
|
||||||
|
{\center
|
||||||
|
\vspace*{3cm}
|
||||||
|
\includegraphics{logo.pdf}
|
||||||
|
\vspace{3cm}
|
||||||
|
\par
|
||||||
|
{\rm\Huge \@title \par}%
|
||||||
|
{\em\LARGE \py@release\releaseinfo \par}
|
||||||
|
{\large
|
||||||
|
\@date \par
|
||||||
|
\py@authoraddress \par
|
||||||
|
}}%
|
||||||
|
\end{flushright}%\par
|
||||||
|
\@thanks
|
||||||
|
\end{titlepage}%
|
||||||
|
\cleardoublepage%
|
||||||
|
\setcounter{footnote}{0}%
|
||||||
|
\let\thanks\relax\let\maketitle\relax
|
||||||
|
%\gdef\@thanks{}\gdef\@author{}\gdef\@title{}
|
||||||
|
}
|
||||||
|
|
||||||
|
\fancypagestyle{normal}{
|
||||||
|
\fancyhf{}
|
||||||
|
\fancyfoot[LE,RO]{{\thepage}}
|
||||||
|
\fancyfoot[LO]{{\nouppercase{\rightmark}}}
|
||||||
|
\fancyfoot[RE]{{\nouppercase{\leftmark}}}
|
||||||
|
\fancyhead[LE,RO]{{ \@title, \py@release}}
|
||||||
|
\renewcommand{\headrulewidth}{0.4pt}
|
||||||
|
\renewcommand{\footrulewidth}{0.4pt}
|
||||||
|
}
|
||||||
|
|
||||||
|
\fancypagestyle{plain}{
|
||||||
|
\fancyhf{}
|
||||||
|
\fancyfoot[LE,RO]{{\thepage}}
|
||||||
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
|
\renewcommand{\footrulewidth}{0.4pt}
|
||||||
|
}
|
||||||
|
|
||||||
|
\titleformat{\section}{\Large}%
|
||||||
|
{\py@TitleColor\thesection}{0.5em}{\py@TitleColor}{\py@NormalColor}
|
||||||
|
\titleformat{\subsection}{\large}%
|
||||||
|
{\py@TitleColor\thesubsection}{0.5em}{\py@TitleColor}{\py@NormalColor}
|
||||||
|
\titleformat{\subsubsection}{}%
|
||||||
|
{\py@TitleColor\thesubsubsection}{0.5em}{\py@TitleColor}{\py@NormalColor}
|
||||||
|
\titleformat{\paragraph}{\large}%
|
||||||
|
{\py@TitleColor}{0em}{\py@TitleColor}{\py@NormalColor}
|
||||||
|
|
||||||
|
\ChNameVar{\raggedleft\normalsize}
|
||||||
|
\ChNumVar{\raggedleft \bfseries\Large}
|
||||||
|
\ChTitleVar{\raggedleft \rm\Huge}
|
||||||
|
|
||||||
|
\renewcommand\thepart{\@Roman\c@part}
|
||||||
|
\renewcommand\part{%
|
||||||
|
\pagestyle{empty}
|
||||||
|
\if@noskipsec \leavevmode \fi
|
||||||
|
\cleardoublepage
|
||||||
|
\vspace*{6cm}%
|
||||||
|
\@afterindentfalse
|
||||||
|
\secdef\@part\@spart}
|
||||||
|
|
||||||
|
\def\@part[#1]#2{%
|
||||||
|
\ifnum \c@secnumdepth >\m@ne
|
||||||
|
\refstepcounter{part}%
|
||||||
|
\addcontentsline{toc}{part}{\thepart\hspace{1em}#1}%
|
||||||
|
\else
|
||||||
|
\addcontentsline{toc}{part}{#1}%
|
||||||
|
\fi
|
||||||
|
{\parindent \z@ %\center
|
||||||
|
\interlinepenalty \@M
|
||||||
|
\normalfont
|
||||||
|
\ifnum \c@secnumdepth >\m@ne
|
||||||
|
\rm\Large \partname~\thepart
|
||||||
|
\par\nobreak
|
||||||
|
\fi
|
||||||
|
\MakeUppercase{\rm\Huge #2}%
|
||||||
|
\markboth{}{}\par}%
|
||||||
|
\nobreak
|
||||||
|
\vskip 8ex
|
||||||
|
\@afterheading}
|
||||||
|
\def\@spart#1{%
|
||||||
|
{\parindent \z@ %\center
|
||||||
|
\interlinepenalty \@M
|
||||||
|
\normalfont
|
||||||
|
\huge \bfseries #1\par}%
|
||||||
|
\nobreak
|
||||||
|
\vskip 3ex
|
||||||
|
\@afterheading}
|
||||||
|
|
||||||
|
% use inconsolata font
|
||||||
|
\usepackage{inconsolata}
|
||||||
|
|
||||||
|
% fix single quotes, for inconsolata. (does not work)
|
||||||
|
%%\usepackage{textcomp}
|
||||||
|
%%\begingroup
|
||||||
|
%% \catcode`'=\active
|
||||||
|
%% \g@addto@macro\@noligs{\let'\textsinglequote}
|
||||||
|
%% \endgroup
|
||||||
|
%%\endinput
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
.. _quickstart:
|
||||||
|
|
||||||
|
==========
|
||||||
|
Quickstart
|
||||||
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Eager to get started?
|
||||||
|
This page gives a good introduction in how to get started with Tablib.
|
||||||
|
This assumes you already have Tablib installed.
|
||||||
|
If you do not, head over to the :ref:`Installation <install>` section.
|
||||||
|
|
||||||
|
First, make sure that:
|
||||||
|
|
||||||
|
* Tablib is :ref:`installed <install>`
|
||||||
|
* Tablib is :ref:`up-to-date <updates>`
|
||||||
|
|
||||||
|
|
||||||
|
Let's get started with some simple use cases and examples.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Creating a Dataset
|
||||||
|
------------------
|
||||||
|
|
||||||
|
|
||||||
|
A :class:`Dataset <tablib.Dataset>` is nothing more than what its name implies—a set of data.
|
||||||
|
|
||||||
|
Creating your own instance of the :class:`tablib.Dataset` object is simple. ::
|
||||||
|
|
||||||
|
data = tablib.Dataset()
|
||||||
|
|
||||||
|
You can now start filling this :class:`Dataset <tablib.Dataset>` object with data.
|
||||||
|
|
||||||
|
.. admonition:: Example Context
|
||||||
|
|
||||||
|
From here on out, if you see ``data``, assume that it's a fresh
|
||||||
|
:class:`Dataset <tablib.Dataset>` object.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-----------
|
||||||
|
Adding Rows
|
||||||
|
-----------
|
||||||
|
|
||||||
|
|
||||||
|
Let's say you want to collect a simple list of names. ::
|
||||||
|
|
||||||
|
# collection of names
|
||||||
|
names = ['Kenneth Reitz', 'Bessie Monke']
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
# split name appropriately
|
||||||
|
fname, lname = name.split()
|
||||||
|
|
||||||
|
# add names to Dataset
|
||||||
|
data.append([fname, lname])
|
||||||
|
|
||||||
|
You can get a nice, Pythonic view of the dataset at any time with :class:`Dataset.dict`::
|
||||||
|
|
||||||
|
>>> data.dict
|
||||||
|
[('Kenneth', 'Reitz'), ('Bessie', 'Monke')]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Adding Headers
|
||||||
|
--------------
|
||||||
|
|
||||||
|
|
||||||
|
It's time to enhance our :class:`Dataset` by giving our columns some titles.
|
||||||
|
To do so, set :class:`Dataset.headers`. ::
|
||||||
|
|
||||||
|
data.headers = ['First Name', 'Last Name']
|
||||||
|
|
||||||
|
Now our data looks a little different. ::
|
||||||
|
|
||||||
|
>>> data.dict
|
||||||
|
[{'Last Name': 'Reitz', 'First Name': 'Kenneth'},
|
||||||
|
{'Last Name': 'Monke', 'First Name': 'Bessie'}]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Adding Columns
|
||||||
|
--------------
|
||||||
|
|
||||||
|
|
||||||
|
Now that we have a basic :class:`Dataset` in place, let's add a column of **ages** to it. ::
|
||||||
|
|
||||||
|
data.append_col([22, 20], header='Age')
|
||||||
|
|
||||||
|
Let's view the data now. ::
|
||||||
|
|
||||||
|
>>> data.dict
|
||||||
|
[{'Last Name': 'Reitz', 'First Name': 'Kenneth', 'Age': 22},
|
||||||
|
{'Last Name': 'Monke', 'First Name': 'Bessie', 'Age': 20}]
|
||||||
|
|
||||||
|
It's that easy.
|
||||||
|
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Importing Data
|
||||||
|
--------------
|
||||||
|
Creating a :class:`tablib.Dataset` object by importing a pre-existing file is simple. ::
|
||||||
|
|
||||||
|
with open('data.csv', 'r') as fh:
|
||||||
|
imported_data = Dataset().load(fh)
|
||||||
|
|
||||||
|
This detects what sort of data is being passed in, and uses an appropriate formatter to do the import. So you can import from a variety of different file types.
|
||||||
|
|
||||||
|
.. admonition:: Source without headers
|
||||||
|
|
||||||
|
When the format is :class:`csv <Dataset.csv>`, :class:`tsv <Dataset.tsv>`, :class:`dbf <Dataset.dbf>`, :class:`xls <Dataset.xls>` or :class:`xlsx <Dataset.xlsx>`, and the data source does not have headers, the import should be done as follows ::
|
||||||
|
|
||||||
|
with open('data.csv', 'r') as fh:
|
||||||
|
imported_data = Dataset().load(fh, headers=False)
|
||||||
|
|
||||||
|
--------------
|
||||||
|
Exporting Data
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Tablib's killer feature is the ability to export your :class:`Dataset` objects into a number of formats.
|
||||||
|
|
||||||
|
**Comma-Separated Values** ::
|
||||||
|
|
||||||
|
>>> data.export('csv')
|
||||||
|
Last Name,First Name,Age
|
||||||
|
Reitz,Kenneth,22
|
||||||
|
Monke,Bessie,20
|
||||||
|
|
||||||
|
**JavaScript Object Notation** ::
|
||||||
|
|
||||||
|
>>> data.export('json')
|
||||||
|
[{"Last Name": "Reitz", "First Name": "Kenneth", "Age": 22}, {"Last Name": "Monke", "First Name": "Bessie", "Age": 20}]
|
||||||
|
|
||||||
|
|
||||||
|
**YAML Ain't Markup Language** ::
|
||||||
|
|
||||||
|
>>> data.export('yaml')
|
||||||
|
- {Age: 22, First Name: Kenneth, Last Name: Reitz}
|
||||||
|
- {Age: 20, First Name: Bessie, Last Name: Monke}
|
||||||
|
|
||||||
|
|
||||||
|
**Microsoft Excel** ::
|
||||||
|
|
||||||
|
>>> data.export('xls')
|
||||||
|
<redacted binary data>
|
||||||
|
|
||||||
|
|
||||||
|
**Pandas DataFrame** ::
|
||||||
|
|
||||||
|
>>> data.export('df')
|
||||||
|
First Name Last Name Age
|
||||||
|
0 Kenneth Reitz 22
|
||||||
|
1 Bessie Monke 21
|
||||||
|
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
Selecting Rows & Columns
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
|
||||||
|
You can slice and dice your data, just like a standard Python list. ::
|
||||||
|
|
||||||
|
>>> data[0]
|
||||||
|
('Kenneth', 'Reitz', 22)
|
||||||
|
|
||||||
|
|
||||||
|
If we had a set of data consisting of thousands of rows,
|
||||||
|
it could be useful to get a list of values in a column.
|
||||||
|
To do so, we access the :class:`Dataset` as if it were a standard Python dictionary. ::
|
||||||
|
|
||||||
|
>>> data['First Name']
|
||||||
|
['Kenneth', 'Bessie']
|
||||||
|
|
||||||
|
You can also access the column using its index. ::
|
||||||
|
|
||||||
|
>>> data.headers
|
||||||
|
['Last Name', 'First Name', 'Age']
|
||||||
|
>>> data.get_col(1)
|
||||||
|
['Kenneth', 'Bessie']
|
||||||
|
|
||||||
|
Let's find the average age. ::
|
||||||
|
|
||||||
|
>>> ages = data['Age']
|
||||||
|
>>> float(sum(ages)) / len(ages)
|
||||||
|
21.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
Removing Rows & Columns
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
It's easier than you could imagine. Delete a column::
|
||||||
|
|
||||||
|
>>> del data['Col Name']
|
||||||
|
|
||||||
|
Delete a range of rows::
|
||||||
|
|
||||||
|
>>> del data[0:12]
|
||||||
|
|
||||||
|
|
||||||
|
==============
|
||||||
|
Advanced Usage
|
||||||
|
==============
|
||||||
|
|
||||||
|
This part of the documentation services to give you an idea that are otherwise hard to extract from the :ref:`API Documentation <api>`.
|
||||||
|
|
||||||
|
And now for something completely different.
|
||||||
|
|
||||||
|
|
||||||
|
.. _dyncols:
|
||||||
|
|
||||||
|
---------------
|
||||||
|
Dynamic Columns
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. versionadded:: 0.8.3
|
||||||
|
|
||||||
|
Thanks to Josh Ourisman, Tablib now supports adding dynamic columns.
|
||||||
|
A dynamic column is a single callable object (*e.g.* a function).
|
||||||
|
|
||||||
|
Let's add a dynamic column to our :class:`Dataset` object.
|
||||||
|
In this example, we have a function that generates a random grade for our students. ::
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
def random_grade(row):
|
||||||
|
"""Returns a random integer for entry."""
|
||||||
|
return (random.randint(60,100)/100.0)
|
||||||
|
|
||||||
|
data.append_col(random_grade, header='Grade')
|
||||||
|
|
||||||
|
Let's have a look at our data. ::
|
||||||
|
|
||||||
|
>>> data.export('yaml')
|
||||||
|
- {Age: 22, First Name: Kenneth, Grade: 0.6, Last Name: Reitz}
|
||||||
|
- {Age: 20, First Name: Bessie, Grade: 0.75, Last Name: Monke}
|
||||||
|
|
||||||
|
|
||||||
|
Let's remove that column. ::
|
||||||
|
|
||||||
|
>>> del data['Grade']
|
||||||
|
|
||||||
|
|
||||||
|
When you add a dynamic column, the first argument that is passed in to the given callable is the current data row.
|
||||||
|
You can use this to perform calculations against your data row.
|
||||||
|
|
||||||
|
For example, we can use the data available in the row to guess the gender of a student. ::
|
||||||
|
|
||||||
|
def guess_gender(row):
|
||||||
|
"""Calculates gender of given student data row."""
|
||||||
|
m_names = ('Kenneth', 'Mike', 'Yuri')
|
||||||
|
f_names = ('Bessie', 'Samantha', 'Heather')
|
||||||
|
|
||||||
|
name = row[0]
|
||||||
|
|
||||||
|
if name in m_names:
|
||||||
|
return 'Male'
|
||||||
|
elif name in f_names:
|
||||||
|
return 'Female'
|
||||||
|
else:
|
||||||
|
return 'Unknown'
|
||||||
|
|
||||||
|
Adding this function to our dataset as a dynamic column would result in: ::
|
||||||
|
|
||||||
|
>>> data.export('yaml')
|
||||||
|
- {Age: 22, First Name: Kenneth, Gender: Male, Last Name: Reitz}
|
||||||
|
- {Age: 20, First Name: Bessie, Gender: Female, Last Name: Monke}
|
||||||
|
|
||||||
|
|
||||||
|
.. _tags:
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
Filtering Datasets with Tags
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 0.9.0
|
||||||
|
|
||||||
|
|
||||||
|
When constructing a :class:`Dataset` object,
|
||||||
|
you can add tags to rows by specifying the ``tags`` parameter.
|
||||||
|
This allows you to filter your :class:`Dataset` later.
|
||||||
|
This can be useful to separate rows of data based on arbitrary criteria
|
||||||
|
(*e.g.* origin) that you don't want to include in your :class:`Dataset`.
|
||||||
|
|
||||||
|
Let's tag some students. ::
|
||||||
|
|
||||||
|
students = tablib.Dataset()
|
||||||
|
|
||||||
|
students.headers = ['first', 'last']
|
||||||
|
|
||||||
|
students.rpush(['Kenneth', 'Reitz'], tags=['male', 'technical'])
|
||||||
|
students.rpush(['Daniel', 'Dupont'], tags=['male', 'creative' ])
|
||||||
|
students.rpush(['Bessie', 'Monke'], tags=['female', 'creative'])
|
||||||
|
|
||||||
|
Now that we have extra meta-data on our rows, we can easily filter our :class:`Dataset`. Let's just see Female students. ::
|
||||||
|
|
||||||
|
>>> students.filter(['female']).yaml
|
||||||
|
- {first: Bessie, Last: Monke}
|
||||||
|
|
||||||
|
By default, when you pass a list of tags you get filter type or. ::
|
||||||
|
|
||||||
|
>>> students.filter(['female', 'creative']).yaml
|
||||||
|
- {first: Daniel, Last: Dupont}
|
||||||
|
- {first: Bessie, Last: Monke}
|
||||||
|
|
||||||
|
Using chaining you can get a filter type and. ::
|
||||||
|
|
||||||
|
>>> students.filter(['female']).filter(['creative']).yaml
|
||||||
|
- {first: Bessie, Last: Monke}
|
||||||
|
|
||||||
|
It's that simple. The original :class:`Dataset` is untouched.
|
||||||
|
|
||||||
|
Open an Excel Workbook and read first sheet
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Open an Excel 2007 and later workbook with a single sheet (or a workbook with multiple sheets but you just want the first sheet). ::
|
||||||
|
|
||||||
|
data = tablib.Dataset()
|
||||||
|
with open('my_excel_file.xlsx', 'rb') as fh:
|
||||||
|
data.load(fh, 'xlsx')
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
Excel Workbook With Multiple Sheets
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
When dealing with a large number of :class:`Datasets <Dataset>` in spreadsheet format,
|
||||||
|
it's quite common to group multiple spreadsheets into a single Excel file, known as a Workbook.
|
||||||
|
Tablib makes it extremely easy to build workbooks with the handy :class:`Databook` class.
|
||||||
|
|
||||||
|
Let's say we have 3 different :class:`Datasets <Dataset>`.
|
||||||
|
All we have to do is add them to a :class:`Databook` object... ::
|
||||||
|
|
||||||
|
book = tablib.Databook((data1, data2, data3))
|
||||||
|
|
||||||
|
... and export to Excel just like :class:`Datasets <Dataset>`. ::
|
||||||
|
|
||||||
|
with open('students.xls', 'wb') as f:
|
||||||
|
f.write(book.export('xls'))
|
||||||
|
|
||||||
|
The resulting ``students.xls`` file will contain a separate spreadsheet for each :class:`Dataset` object in the :class:`Databook`.
|
||||||
|
|
||||||
|
.. admonition:: Binary Warning
|
||||||
|
|
||||||
|
Make sure to open the output file in binary mode.
|
||||||
|
|
||||||
|
|
||||||
|
.. _separators:
|
||||||
|
|
||||||
|
----------
|
||||||
|
Separators
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. versionadded:: 0.8.2
|
||||||
|
|
||||||
|
When constructing a spreadsheet,
|
||||||
|
it's often useful to create a blank row containing information on the upcoming data. So,
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
daniel_tests = [
|
||||||
|
('11/24/09', 'Math 101 Mid-term Exam', 56.),
|
||||||
|
('05/24/10', 'Math 101 Final Exam', 62.)
|
||||||
|
]
|
||||||
|
|
||||||
|
suzie_tests = [
|
||||||
|
('11/24/09', 'Math 101 Mid-term Exam', 56.),
|
||||||
|
('05/24/10', 'Math 101 Final Exam', 62.)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create new dataset
|
||||||
|
tests = tablib.Dataset()
|
||||||
|
tests.headers = ['Date', 'Test Name', 'Grade']
|
||||||
|
|
||||||
|
# Daniel's Tests
|
||||||
|
tests.append_separator('Daniel\'s Scores')
|
||||||
|
|
||||||
|
for test_row in daniel_tests:
|
||||||
|
tests.append(test_row)
|
||||||
|
|
||||||
|
# Susie's Tests
|
||||||
|
tests.append_separator('Susie\'s Scores')
|
||||||
|
|
||||||
|
for test_row in suzie_tests:
|
||||||
|
tests.append(test_row)
|
||||||
|
|
||||||
|
# Write spreadsheet to disk
|
||||||
|
with open('grades.xls', 'wb') as f:
|
||||||
|
f.write(tests.export('xls'))
|
||||||
|
|
||||||
|
The resulting **tests.xls** will have the following layout:
|
||||||
|
|
||||||
|
|
||||||
|
Daniel's Scores:
|
||||||
|
* '11/24/09', 'Math 101 Mid-term Exam', 56.
|
||||||
|
* '05/24/10', 'Math 101 Final Exam', 62.
|
||||||
|
|
||||||
|
Suzie's Scores:
|
||||||
|
* '11/24/09', 'Math 101 Mid-term Exam', 56.
|
||||||
|
* '05/24/10', 'Math 101 Final Exam', 62.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. admonition:: Format Support
|
||||||
|
|
||||||
|
At this time, only :class:`Excel <Dataset.xls>` output supports separators.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Now, go check out the :ref:`API Documentation <api>` or begin :ref:`Tablib Development <development>`.
|
||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
from fabric.api import *
|
|
||||||
|
|
||||||
|
|
||||||
def scrub():
|
|
||||||
""" Death to the bytecode! """
|
|
||||||
local("rm -fr dist build")
|
|
||||||
local("find . -name \"*.pyc\" -exec rm '{}' ';'")
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
norecursedirs = .git .*
|
||||||
|
addopts = -rsxX --showlocals --tb=native --cov=tablib --cov=tests --cov-report xml --cov-report term --cov-report html
|
||||||
@@ -1,43 +1,52 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
from setuptools import find_packages, setup
|
||||||
import sys
|
|
||||||
|
|
||||||
from distutils.core import setup
|
|
||||||
|
|
||||||
|
|
||||||
def publish():
|
|
||||||
"""Publish to PyPi"""
|
|
||||||
os.system("python setup.py sdist upload")
|
|
||||||
|
|
||||||
if sys.argv[-1] == "publish":
|
|
||||||
publish()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='tablib',
|
name='tablib',
|
||||||
version='0.8.1',
|
use_scm_version={
|
||||||
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
|
'write_to': 'src/tablib/_version.py',
|
||||||
long_description=open('README.rst').read() + '\n\n' +
|
},
|
||||||
open('HISTORY.rst').read(),
|
setup_requires=['setuptools_scm'],
|
||||||
author='Kenneth Reitz',
|
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
|
||||||
author_email='me@kennethreitz.com',
|
long_description=(
|
||||||
url='http://github.com/kennethreitz/tablib',
|
open('README.md').read() + '\n\n' + open('HISTORY.md').read()
|
||||||
packages=['tablib', 'tablib.formats'],
|
),
|
||||||
install_requires=['xlwt', 'simplejson', 'PyYAML'],
|
long_description_content_type="text/markdown",
|
||||||
license='MIT',
|
author='Kenneth Reitz',
|
||||||
classifiers=(
|
author_email='me@kennethreitz.org',
|
||||||
'Development Status :: 4 - Beta',
|
maintainer='Jazzband',
|
||||||
'License :: OSI Approved :: MIT License',
|
maintainer_email='roadies@jazzband.co',
|
||||||
'Programming Language :: Python',
|
url='https://tablib.readthedocs.io',
|
||||||
# 'Programming Language :: Python :: 2.5',
|
project_urls={
|
||||||
'Programming Language :: Python :: 2.6',
|
"Documentation": "https://tablib.readthedocs.io",
|
||||||
'Programming Language :: Python :: 2.7',
|
"Source": "https://github.com/jazzband/tablib",
|
||||||
),
|
},
|
||||||
# entry_points={
|
packages=find_packages(where="src"),
|
||||||
# 'console_scripts': [
|
package_dir={"": "src"},
|
||||||
# 'tabbed = tablib.cli:start',
|
license='MIT',
|
||||||
# ],
|
classifiers=[
|
||||||
# }
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Natural Language :: English',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
],
|
||||||
|
python_requires='>=3.6',
|
||||||
|
extras_require={
|
||||||
|
'all': ['markuppy', 'odfpy', 'openpyxl>=2.6.0', 'pandas', 'pyyaml', 'tabulate', 'xlrd', 'xlwt'],
|
||||||
|
'cli': ['tabulate'],
|
||||||
|
'html': ['markuppy'],
|
||||||
|
'ods': ['odfpy'],
|
||||||
|
'pandas': ['pandas'],
|
||||||
|
'xls': ['xlrd', 'xlwt'],
|
||||||
|
'xlsx': ['openpyxl>=2.6.0'],
|
||||||
|
'yaml': ['pyyaml'],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
""" Tablib. """
|
||||||
|
try:
|
||||||
|
# Generated by setuptools-scm.
|
||||||
|
from ._version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
# Some broken installation.
|
||||||
|
__version__ = None
|
||||||
|
|
||||||
|
|
||||||
|
from tablib.core import ( # noqa: F401
|
||||||
|
Databook,
|
||||||
|
Dataset,
|
||||||
|
InvalidDatasetType,
|
||||||
|
InvalidDimensions,
|
||||||
|
UnsupportedFormat,
|
||||||
|
detect_format,
|
||||||
|
import_book,
|
||||||
|
import_set,
|
||||||
|
)
|
||||||
@@ -0,0 +1,917 @@
|
|||||||
|
"""
|
||||||
|
tablib.core
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module implements the central Tablib objects.
|
||||||
|
|
||||||
|
:copyright: (c) 2016 by Kenneth Reitz. 2019 Jazzband.
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from copy import copy
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
from tablib.exceptions import (
|
||||||
|
HeadersNeeded,
|
||||||
|
InvalidDatasetIndex,
|
||||||
|
InvalidDatasetType,
|
||||||
|
InvalidDimensions,
|
||||||
|
UnsupportedFormat,
|
||||||
|
)
|
||||||
|
from tablib.formats import registry
|
||||||
|
from tablib.utils import normalize_input
|
||||||
|
|
||||||
|
__title__ = 'tablib'
|
||||||
|
__author__ = 'Kenneth Reitz'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__copyright__ = 'Copyright 2017 Kenneth Reitz. 2019 Jazzband.'
|
||||||
|
__docformat__ = 'restructuredtext'
|
||||||
|
|
||||||
|
|
||||||
|
class Row:
|
||||||
|
"""Internal Row object. Mainly used for filtering."""
|
||||||
|
|
||||||
|
__slots__ = ['_row', 'tags']
|
||||||
|
|
||||||
|
def __init__(self, row=list(), tags=list()):
|
||||||
|
self._row = list(row)
|
||||||
|
self.tags = list(tags)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (col for col in self._row)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._row)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self._row)
|
||||||
|
|
||||||
|
def __getitem__(self, i):
|
||||||
|
return self._row[i]
|
||||||
|
|
||||||
|
def __setitem__(self, i, value):
|
||||||
|
self._row[i] = value
|
||||||
|
|
||||||
|
def __delitem__(self, i):
|
||||||
|
del self._row[i]
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return self._row, self.tags
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
self._row, self.tags = state
|
||||||
|
|
||||||
|
def rpush(self, value):
|
||||||
|
self.insert(len(self._row), value)
|
||||||
|
|
||||||
|
def lpush(self, value):
|
||||||
|
self.insert(0, value)
|
||||||
|
|
||||||
|
def append(self, value):
|
||||||
|
self.rpush(value)
|
||||||
|
|
||||||
|
def insert(self, index, value):
|
||||||
|
self._row.insert(index, value)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return (item in self._row)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tuple(self):
|
||||||
|
"""Tuple representation of :class:`Row`."""
|
||||||
|
return tuple(self._row)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list(self):
|
||||||
|
"""List representation of :class:`Row`."""
|
||||||
|
return list(self._row)
|
||||||
|
|
||||||
|
def has_tag(self, tag):
|
||||||
|
"""Returns true if current row contains tag."""
|
||||||
|
|
||||||
|
if tag is None:
|
||||||
|
return False
|
||||||
|
elif isinstance(tag, str):
|
||||||
|
return (tag in self.tags)
|
||||||
|
else:
|
||||||
|
return bool(len(set(tag) & set(self.tags)))
|
||||||
|
|
||||||
|
|
||||||
|
class Dataset:
|
||||||
|
"""The :class:`Dataset` object is the heart of Tablib. It provides all core
|
||||||
|
functionality.
|
||||||
|
|
||||||
|
Usually you create a :class:`Dataset` instance in your main module, and append
|
||||||
|
rows as you collect data. ::
|
||||||
|
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.headers = ('name', 'age')
|
||||||
|
|
||||||
|
for (name, age) in some_collector():
|
||||||
|
data.append((name, age))
|
||||||
|
|
||||||
|
|
||||||
|
Setting columns is similar. The column data length must equal the
|
||||||
|
current height of the data and headers must be set. ::
|
||||||
|
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.headers = ('first_name', 'last_name')
|
||||||
|
|
||||||
|
data.append(('John', 'Adams'))
|
||||||
|
data.append(('George', 'Washington'))
|
||||||
|
|
||||||
|
data.append_col((90, 67), header='age')
|
||||||
|
|
||||||
|
|
||||||
|
You can also set rows and headers upon instantiation. This is useful if
|
||||||
|
dealing with dozens or hundreds of :class:`Dataset` objects. ::
|
||||||
|
|
||||||
|
headers = ('first_name', 'last_name')
|
||||||
|
data = [('John', 'Adams'), ('George', 'Washington')]
|
||||||
|
|
||||||
|
data = tablib.Dataset(*data, headers=headers)
|
||||||
|
|
||||||
|
:param \\*args: (optional) list of rows to populate Dataset
|
||||||
|
:param headers: (optional) list strings for Dataset header row
|
||||||
|
:param title: (optional) string to use as title of the Dataset
|
||||||
|
|
||||||
|
|
||||||
|
.. admonition:: Format Attributes Definition
|
||||||
|
|
||||||
|
If you look at the code, the various output/import formats are not
|
||||||
|
defined within the :class:`Dataset` object. To add support for a new format, see
|
||||||
|
:ref:`Adding New Formats <newformats>`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._data = list(Row(arg) for arg in args)
|
||||||
|
self.__headers = None
|
||||||
|
|
||||||
|
# ('title', index) tuples
|
||||||
|
self._separators = []
|
||||||
|
|
||||||
|
# (column, callback) tuples
|
||||||
|
self._formatters = []
|
||||||
|
|
||||||
|
self.headers = kwargs.get('headers')
|
||||||
|
|
||||||
|
self.title = kwargs.get('title')
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.height
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, str):
|
||||||
|
if key in self.headers:
|
||||||
|
pos = self.headers.index(key) # get 'key' index from each data
|
||||||
|
return [row[pos] for row in self._data]
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
else:
|
||||||
|
_results = self._data[key]
|
||||||
|
if isinstance(_results, Row):
|
||||||
|
return _results.tuple
|
||||||
|
else:
|
||||||
|
return [result.tuple for result in _results]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._validate(value)
|
||||||
|
self._data[key] = Row(value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
if isinstance(key, str):
|
||||||
|
|
||||||
|
if key in self.headers:
|
||||||
|
|
||||||
|
pos = self.headers.index(key)
|
||||||
|
del self.headers[pos]
|
||||||
|
|
||||||
|
for i, row in enumerate(self._data):
|
||||||
|
|
||||||
|
del row[pos]
|
||||||
|
self._data[i] = row
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
else:
|
||||||
|
del self._data[key]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
try:
|
||||||
|
return '<%s dataset>' % (self.title.lower())
|
||||||
|
except AttributeError:
|
||||||
|
return '<dataset object>'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
result = []
|
||||||
|
|
||||||
|
# Add str representation of headers.
|
||||||
|
if self.__headers:
|
||||||
|
result.append([str(h) for h in self.__headers])
|
||||||
|
|
||||||
|
# Add str representation of rows.
|
||||||
|
result.extend(list(map(str, row)) for row in self._data)
|
||||||
|
|
||||||
|
lens = [list(map(len, row)) for row in result]
|
||||||
|
field_lens = list(map(max, zip(*lens)))
|
||||||
|
|
||||||
|
# delimiter between header and data
|
||||||
|
if self.__headers:
|
||||||
|
result.insert(1, ['-' * length for length in field_lens])
|
||||||
|
|
||||||
|
format_string = '|'.join('{%s:%s}' % item for item in enumerate(field_lens))
|
||||||
|
|
||||||
|
return '\n'.join(format_string.format(*row) for row in result)
|
||||||
|
|
||||||
|
# ---------
|
||||||
|
# Internals
|
||||||
|
# ---------
|
||||||
|
|
||||||
|
def _get_in_format(self, fmt_key, **kwargs):
|
||||||
|
return registry.get_format(fmt_key).export_set(self, **kwargs)
|
||||||
|
|
||||||
|
def _set_in_format(self, fmt_key, in_stream, **kwargs):
|
||||||
|
in_stream = normalize_input(in_stream)
|
||||||
|
return registry.get_format(fmt_key).import_set(self, in_stream, **kwargs)
|
||||||
|
|
||||||
|
def _validate(self, row=None, col=None, safety=False):
|
||||||
|
"""Assures size of every row in dataset is of proper proportions."""
|
||||||
|
if row:
|
||||||
|
is_valid = (len(row) == self.width) if self.width else True
|
||||||
|
elif col:
|
||||||
|
if len(col) < 1:
|
||||||
|
is_valid = True
|
||||||
|
else:
|
||||||
|
is_valid = (len(col) == self.height) if self.height else True
|
||||||
|
else:
|
||||||
|
is_valid = all(len(x) == self.width for x in self._data)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if not safety:
|
||||||
|
raise InvalidDimensions
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _package(self, dicts=True, ordered=True):
|
||||||
|
"""Packages Dataset into lists of dictionaries for transmission."""
|
||||||
|
# TODO: Dicts default to false?
|
||||||
|
|
||||||
|
_data = list(self._data)
|
||||||
|
|
||||||
|
if ordered:
|
||||||
|
dict_pack = OrderedDict
|
||||||
|
else:
|
||||||
|
dict_pack = dict
|
||||||
|
|
||||||
|
# Execute formatters
|
||||||
|
if self._formatters:
|
||||||
|
for row_i, row in enumerate(_data):
|
||||||
|
for col, callback in self._formatters:
|
||||||
|
try:
|
||||||
|
if col is None:
|
||||||
|
for j, c in enumerate(row):
|
||||||
|
_data[row_i][j] = callback(c)
|
||||||
|
else:
|
||||||
|
_data[row_i][col] = callback(row[col])
|
||||||
|
except IndexError:
|
||||||
|
raise InvalidDatasetIndex
|
||||||
|
|
||||||
|
if self.headers:
|
||||||
|
if dicts:
|
||||||
|
data = [dict_pack(list(zip(self.headers, data_row))) for data_row in _data]
|
||||||
|
else:
|
||||||
|
data = [list(self.headers)] + list(_data)
|
||||||
|
else:
|
||||||
|
data = [list(row) for row in _data]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _get_headers(self):
|
||||||
|
"""An *optional* list of strings to be used for header rows and attribute names.
|
||||||
|
|
||||||
|
This must be set manually. The given list length must equal :attr:`Dataset.width`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.__headers
|
||||||
|
|
||||||
|
def _set_headers(self, collection):
|
||||||
|
"""Validating headers setter."""
|
||||||
|
self._validate(collection)
|
||||||
|
if collection:
|
||||||
|
try:
|
||||||
|
self.__headers = list(collection)
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError
|
||||||
|
else:
|
||||||
|
self.__headers = None
|
||||||
|
|
||||||
|
headers = property(_get_headers, _set_headers)
|
||||||
|
|
||||||
|
def _get_dict(self):
|
||||||
|
"""A native Python representation of the :class:`Dataset` object. If headers have
|
||||||
|
been set, a list of Python dictionaries will be returned. If no headers have been set,
|
||||||
|
a list of tuples (rows) will be returned instead.
|
||||||
|
|
||||||
|
A dataset object can also be imported by setting the `Dataset.dict` attribute: ::
|
||||||
|
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}]
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._package()
|
||||||
|
|
||||||
|
def _set_dict(self, pickle):
|
||||||
|
"""A native Python representation of the Dataset object. If headers have been
|
||||||
|
set, a list of Python dictionaries will be returned. If no headers have been
|
||||||
|
set, a list of tuples (rows) will be returned instead.
|
||||||
|
|
||||||
|
A dataset object can also be imported by setting the :attr:`Dataset.dict` attribute. ::
|
||||||
|
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not len(pickle):
|
||||||
|
return
|
||||||
|
|
||||||
|
# if list of rows
|
||||||
|
if isinstance(pickle[0], list):
|
||||||
|
self.wipe()
|
||||||
|
for row in pickle:
|
||||||
|
self.append(Row(row))
|
||||||
|
|
||||||
|
# if list of objects
|
||||||
|
elif isinstance(pickle[0], dict):
|
||||||
|
self.wipe()
|
||||||
|
self.headers = list(pickle[0].keys())
|
||||||
|
for row in pickle:
|
||||||
|
self.append(Row(list(row.values())))
|
||||||
|
else:
|
||||||
|
raise UnsupportedFormat
|
||||||
|
|
||||||
|
dict = property(_get_dict, _set_dict)
|
||||||
|
|
||||||
|
def _clean_col(self, col):
|
||||||
|
"""Prepares the given column for insert/append."""
|
||||||
|
|
||||||
|
col = list(col)
|
||||||
|
|
||||||
|
if self.headers:
|
||||||
|
header = [col.pop(0)]
|
||||||
|
else:
|
||||||
|
header = []
|
||||||
|
|
||||||
|
if len(col) == 1 and hasattr(col[0], '__call__'):
|
||||||
|
|
||||||
|
col = list(map(col[0], self._data))
|
||||||
|
col = tuple(header + col)
|
||||||
|
|
||||||
|
return col
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
"""The number of rows currently in the :class:`Dataset`.
|
||||||
|
Cannot be directly modified.
|
||||||
|
"""
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
"""The number of columns currently in the :class:`Dataset`.
|
||||||
|
Cannot be directly modified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return len(self._data[0])
|
||||||
|
except IndexError:
|
||||||
|
try:
|
||||||
|
return len(self.headers)
|
||||||
|
except TypeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def load(self, in_stream, format=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Import `in_stream` to the :class:`Dataset` object using the `format`.
|
||||||
|
`in_stream` can be a file-like object, a string, or a bytestring.
|
||||||
|
|
||||||
|
:param \\*\\*kwargs: (optional) custom configuration to the format `import_set`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stream = normalize_input(in_stream)
|
||||||
|
if not format:
|
||||||
|
format = detect_format(stream)
|
||||||
|
|
||||||
|
fmt = registry.get_format(format)
|
||||||
|
if not hasattr(fmt, 'import_set'):
|
||||||
|
raise UnsupportedFormat(f'Format {format} cannot be imported.')
|
||||||
|
|
||||||
|
if not import_set:
|
||||||
|
raise UnsupportedFormat(f'Format {format} cannot be imported.')
|
||||||
|
|
||||||
|
fmt.import_set(self, stream, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self, format, **kwargs):
|
||||||
|
"""
|
||||||
|
Export :class:`Dataset` object to `format`.
|
||||||
|
|
||||||
|
:param \\*\\*kwargs: (optional) custom configuration to the format `export_set`.
|
||||||
|
"""
|
||||||
|
fmt = registry.get_format(format)
|
||||||
|
if not hasattr(fmt, 'export_set'):
|
||||||
|
raise UnsupportedFormat(f'Format {format} cannot be exported.')
|
||||||
|
|
||||||
|
return fmt.export_set(self, **kwargs)
|
||||||
|
|
||||||
|
# ----
|
||||||
|
# Rows
|
||||||
|
# ----
|
||||||
|
|
||||||
|
def insert(self, index, row, tags=list()):
|
||||||
|
"""Inserts a row to the :class:`Dataset` at the given index.
|
||||||
|
|
||||||
|
Rows inserted must be the correct size (height or width).
|
||||||
|
|
||||||
|
The default behaviour is to insert the given row to the :class:`Dataset`
|
||||||
|
object at the given index.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._validate(row)
|
||||||
|
self._data.insert(index, Row(row, tags=tags))
|
||||||
|
|
||||||
|
def rpush(self, row, tags=list()):
|
||||||
|
"""Adds a row to the end of the :class:`Dataset`.
|
||||||
|
See :method:`Dataset.insert` for additional documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.insert(self.height, row=row, tags=tags)
|
||||||
|
|
||||||
|
def lpush(self, row, tags=list()):
|
||||||
|
"""Adds a row to the top of the :class:`Dataset`.
|
||||||
|
See :method:`Dataset.insert` for additional documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.insert(0, row=row, tags=tags)
|
||||||
|
|
||||||
|
def append(self, row, tags=list()):
|
||||||
|
"""Adds a row to the :class:`Dataset`.
|
||||||
|
See :method:`Dataset.insert` for additional documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rpush(row, tags)
|
||||||
|
|
||||||
|
def extend(self, rows, tags=list()):
|
||||||
|
"""Adds a list of rows to the :class:`Dataset` using
|
||||||
|
:method:`Dataset.append`
|
||||||
|
"""
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
self.append(row, tags)
|
||||||
|
|
||||||
|
def lpop(self):
|
||||||
|
"""Removes and returns the first row of the :class:`Dataset`."""
|
||||||
|
|
||||||
|
cache = self[0]
|
||||||
|
del self[0]
|
||||||
|
|
||||||
|
return cache
|
||||||
|
|
||||||
|
def rpop(self):
|
||||||
|
"""Removes and returns the last row of the :class:`Dataset`."""
|
||||||
|
|
||||||
|
cache = self[-1]
|
||||||
|
del self[-1]
|
||||||
|
|
||||||
|
return cache
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
"""Removes and returns the last row of the :class:`Dataset`."""
|
||||||
|
|
||||||
|
return self.rpop()
|
||||||
|
|
||||||
|
# -------
|
||||||
|
# Columns
|
||||||
|
# -------
|
||||||
|
|
||||||
|
def insert_col(self, index, col=None, header=None):
|
||||||
|
"""Inserts a column to the :class:`Dataset` at the given index.
|
||||||
|
|
||||||
|
Columns inserted must be the correct height.
|
||||||
|
|
||||||
|
You can also insert a column of a single callable object, which will
|
||||||
|
add a new column with the return values of the callable each as an
|
||||||
|
item in the column. ::
|
||||||
|
|
||||||
|
data.append_col(col=random.randint)
|
||||||
|
|
||||||
|
If inserting a column, and :attr:`Dataset.headers` is set, the
|
||||||
|
header attribute must be set, and will be considered the header for
|
||||||
|
that row.
|
||||||
|
|
||||||
|
See :ref:`dyncols` for an in-depth example.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.9.0
|
||||||
|
If inserting a column, and :attr:`Dataset.headers` is set, the
|
||||||
|
header attribute must be set, and will be considered the header for
|
||||||
|
that row.
|
||||||
|
|
||||||
|
.. versionadded:: 0.9.0
|
||||||
|
If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting.
|
||||||
|
This gives you the ability to :method:`filter <Dataset.filter>` your
|
||||||
|
:class:`Dataset` later.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if col is None:
|
||||||
|
col = []
|
||||||
|
|
||||||
|
# Callable Columns...
|
||||||
|
if hasattr(col, '__call__'):
|
||||||
|
col = list(map(col, self._data))
|
||||||
|
|
||||||
|
col = self._clean_col(col)
|
||||||
|
self._validate(col=col)
|
||||||
|
|
||||||
|
if self.headers:
|
||||||
|
# pop the first item off, add to headers
|
||||||
|
if not header:
|
||||||
|
raise HeadersNeeded()
|
||||||
|
|
||||||
|
# corner case - if header is set without data
|
||||||
|
elif header and self.height == 0 and len(col):
|
||||||
|
raise InvalidDimensions
|
||||||
|
|
||||||
|
self.headers.insert(index, header)
|
||||||
|
|
||||||
|
if self.height and self.width:
|
||||||
|
|
||||||
|
for i, row in enumerate(self._data):
|
||||||
|
|
||||||
|
row.insert(index, col[i])
|
||||||
|
self._data[i] = row
|
||||||
|
else:
|
||||||
|
self._data = [Row([row]) for row in col]
|
||||||
|
|
||||||
|
def rpush_col(self, col, header=None):
|
||||||
|
"""Adds a column to the end of the :class:`Dataset`.
|
||||||
|
See :method:`Dataset.insert` for additional documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.insert_col(self.width, col, header=header)
|
||||||
|
|
||||||
|
def lpush_col(self, col, header=None):
|
||||||
|
"""Adds a column to the top of the :class:`Dataset`.
|
||||||
|
See :method:`Dataset.insert` for additional documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.insert_col(0, col, header=header)
|
||||||
|
|
||||||
|
def insert_separator(self, index, text='-'):
|
||||||
|
"""Adds a separator to :class:`Dataset` at given index."""
|
||||||
|
|
||||||
|
sep = (index, text)
|
||||||
|
self._separators.append(sep)
|
||||||
|
|
||||||
|
def append_separator(self, text='-'):
|
||||||
|
"""Adds a :ref:`separator <separators>` to the :class:`Dataset`."""
|
||||||
|
|
||||||
|
# change offsets if headers are or aren't defined
|
||||||
|
if not self.headers:
|
||||||
|
index = self.height if self.height else 0
|
||||||
|
else:
|
||||||
|
index = (self.height + 1) if self.height else 1
|
||||||
|
|
||||||
|
self.insert_separator(index, text)
|
||||||
|
|
||||||
|
def append_col(self, col, header=None):
|
||||||
|
"""Adds a column to the :class:`Dataset`.
|
||||||
|
See :method:`Dataset.insert_col` for additional documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rpush_col(col, header)
|
||||||
|
|
||||||
|
def get_col(self, index):
|
||||||
|
"""Returns the column from the :class:`Dataset` at the given index."""
|
||||||
|
|
||||||
|
return [row[index] for row in self._data]
|
||||||
|
|
||||||
|
# ----
|
||||||
|
# Misc
|
||||||
|
# ----
|
||||||
|
|
||||||
|
def add_formatter(self, col, handler):
|
||||||
|
"""Adds a formatter to the :class:`Dataset`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.9.5
|
||||||
|
|
||||||
|
:param col: column to. Accepts index int or header str.
|
||||||
|
:param handler: reference to callback function to execute against
|
||||||
|
each cell value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(col, str):
|
||||||
|
if col in self.headers:
|
||||||
|
col = self.headers.index(col) # get 'key' index from each data
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
if not col > self.width:
|
||||||
|
self._formatters.append((col, handler))
|
||||||
|
else:
|
||||||
|
raise InvalidDatasetIndex
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def filter(self, tag):
|
||||||
|
"""Returns a new instance of the :class:`Dataset`, excluding any rows
|
||||||
|
that do not contain the given :ref:`tags <tags>`.
|
||||||
|
"""
|
||||||
|
_dset = copy(self)
|
||||||
|
_dset._data = [row for row in _dset._data if row.has_tag(tag)]
|
||||||
|
|
||||||
|
return _dset
|
||||||
|
|
||||||
|
def sort(self, col, reverse=False):
|
||||||
|
"""Sort a :class:`Dataset` by a specific column, given string (for
|
||||||
|
header) or integer (for column index). The order can be reversed by
|
||||||
|
setting ``reverse`` to ``True``.
|
||||||
|
|
||||||
|
Returns a new :class:`Dataset` instance where columns have been
|
||||||
|
sorted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(col, str):
|
||||||
|
|
||||||
|
if not self.headers:
|
||||||
|
raise HeadersNeeded
|
||||||
|
|
||||||
|
_sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
|
||||||
|
_dset = Dataset(headers=self.headers, title=self.title)
|
||||||
|
|
||||||
|
for item in _sorted:
|
||||||
|
row = [item[key] for key in self.headers]
|
||||||
|
_dset.append(row=row)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self.headers:
|
||||||
|
col = self.headers[col]
|
||||||
|
|
||||||
|
_sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
|
||||||
|
_dset = Dataset(headers=self.headers, title=self.title)
|
||||||
|
|
||||||
|
for item in _sorted:
|
||||||
|
if self.headers:
|
||||||
|
row = [item[key] for key in self.headers]
|
||||||
|
else:
|
||||||
|
row = item
|
||||||
|
_dset.append(row=row)
|
||||||
|
|
||||||
|
return _dset
|
||||||
|
|
||||||
|
def transpose(self):
|
||||||
|
"""Transpose a :class:`Dataset`, turning rows into columns and vice
|
||||||
|
versa, returning a new ``Dataset`` instance. The first row of the
|
||||||
|
original instance becomes the new header row."""
|
||||||
|
|
||||||
|
# Don't transpose if there is no data
|
||||||
|
if not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
_dset = Dataset()
|
||||||
|
# The first element of the headers stays in the headers,
|
||||||
|
# it is our "hinge" on which we rotate the data
|
||||||
|
new_headers = [self.headers[0]] + self[self.headers[0]]
|
||||||
|
|
||||||
|
_dset.headers = new_headers
|
||||||
|
for index, column in enumerate(self.headers):
|
||||||
|
|
||||||
|
if column == self.headers[0]:
|
||||||
|
# It's in the headers, so skip it
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Adding the column name as now they're a regular column
|
||||||
|
# Use `get_col(index)` in case there are repeated values
|
||||||
|
row_data = [column] + self.get_col(index)
|
||||||
|
row_data = Row(row_data)
|
||||||
|
_dset.append(row=row_data)
|
||||||
|
return _dset
|
||||||
|
|
||||||
|
def stack(self, other):
|
||||||
|
"""Stack two :class:`Dataset` instances together by
|
||||||
|
joining at the row level, and return new combined
|
||||||
|
``Dataset`` instance."""
|
||||||
|
|
||||||
|
if not isinstance(other, Dataset):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.width != other.width:
|
||||||
|
raise InvalidDimensions
|
||||||
|
|
||||||
|
# Copy the source data
|
||||||
|
_dset = copy(self)
|
||||||
|
|
||||||
|
rows_to_stack = [row for row in _dset._data]
|
||||||
|
other_rows = [row for row in other._data]
|
||||||
|
|
||||||
|
rows_to_stack.extend(other_rows)
|
||||||
|
_dset._data = rows_to_stack
|
||||||
|
|
||||||
|
return _dset
|
||||||
|
|
||||||
|
def stack_cols(self, other):
|
||||||
|
"""Stack two :class:`Dataset` instances together by
|
||||||
|
joining at the column level, and return a new
|
||||||
|
combined ``Dataset`` instance. If either ``Dataset``
|
||||||
|
has headers set, than the other must as well."""
|
||||||
|
|
||||||
|
if not isinstance(other, Dataset):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.headers or other.headers:
|
||||||
|
if not self.headers or not other.headers:
|
||||||
|
raise HeadersNeeded
|
||||||
|
|
||||||
|
if self.height != other.height:
|
||||||
|
raise InvalidDimensions
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_headers = self.headers + other.headers
|
||||||
|
except TypeError:
|
||||||
|
new_headers = None
|
||||||
|
|
||||||
|
_dset = Dataset()
|
||||||
|
|
||||||
|
for column in self.headers:
|
||||||
|
_dset.append_col(col=self[column])
|
||||||
|
|
||||||
|
for column in other.headers:
|
||||||
|
_dset.append_col(col=other[column])
|
||||||
|
|
||||||
|
_dset.headers = new_headers
|
||||||
|
|
||||||
|
return _dset
|
||||||
|
|
||||||
|
def remove_duplicates(self):
|
||||||
|
"""Removes all duplicate rows from the :class:`Dataset` object
|
||||||
|
while maintaining the original order."""
|
||||||
|
seen = set()
|
||||||
|
self._data[:] = [row for row in self._data if not (tuple(row) in seen or seen.add(tuple(row)))]
|
||||||
|
|
||||||
|
def wipe(self):
|
||||||
|
"""Removes all content and headers from the :class:`Dataset` object."""
|
||||||
|
self._data = list()
|
||||||
|
self.__headers = None
|
||||||
|
|
||||||
|
def subset(self, rows=None, cols=None):
|
||||||
|
"""Returns a new instance of the :class:`Dataset`,
|
||||||
|
including only specified rows and columns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Don't return if no data
|
||||||
|
if not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
if rows is None:
|
||||||
|
rows = list(range(self.height))
|
||||||
|
|
||||||
|
if cols is None:
|
||||||
|
cols = list(self.headers)
|
||||||
|
|
||||||
|
# filter out impossible rows and columns
|
||||||
|
rows = [row for row in rows if row in range(self.height)]
|
||||||
|
cols = [header for header in cols if header in self.headers]
|
||||||
|
|
||||||
|
_dset = Dataset()
|
||||||
|
|
||||||
|
# filtering rows and columns
|
||||||
|
_dset.headers = list(cols)
|
||||||
|
|
||||||
|
_dset._data = []
|
||||||
|
for row_no, row in enumerate(self._data):
|
||||||
|
data_row = []
|
||||||
|
for key in _dset.headers:
|
||||||
|
if key in self.headers:
|
||||||
|
pos = self.headers.index(key)
|
||||||
|
data_row.append(row[pos])
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
if row_no in rows:
|
||||||
|
_dset.append(row=Row(data_row))
|
||||||
|
|
||||||
|
return _dset
|
||||||
|
|
||||||
|
|
||||||
|
class Databook:
|
||||||
|
"""A book of :class:`Dataset` objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sets=None):
|
||||||
|
self._datasets = sets or []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
try:
|
||||||
|
return '<%s databook>' % (self.title.lower())
|
||||||
|
except AttributeError:
|
||||||
|
return '<databook object>'
|
||||||
|
|
||||||
|
def wipe(self):
|
||||||
|
"""Removes all :class:`Dataset` objects from the :class:`Databook`."""
|
||||||
|
self._datasets = []
|
||||||
|
|
||||||
|
def sheets(self):
|
||||||
|
return self._datasets
|
||||||
|
|
||||||
|
def add_sheet(self, dataset):
|
||||||
|
"""Adds given :class:`Dataset` to the :class:`Databook`."""
|
||||||
|
if isinstance(dataset, Dataset):
|
||||||
|
self._datasets.append(dataset)
|
||||||
|
else:
|
||||||
|
raise InvalidDatasetType
|
||||||
|
|
||||||
|
def _package(self, ordered=True):
|
||||||
|
"""Packages :class:`Databook` for delivery."""
|
||||||
|
collector = []
|
||||||
|
|
||||||
|
if ordered:
|
||||||
|
dict_pack = OrderedDict
|
||||||
|
else:
|
||||||
|
dict_pack = dict
|
||||||
|
|
||||||
|
for dset in self._datasets:
|
||||||
|
collector.append(dict_pack(
|
||||||
|
title=dset.title,
|
||||||
|
data=dset._package(ordered=ordered)
|
||||||
|
))
|
||||||
|
return collector
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
"""The number of the :class:`Dataset` objects within :class:`Databook`."""
|
||||||
|
return len(self._datasets)
|
||||||
|
|
||||||
|
def load(self, in_stream, format, **kwargs):
|
||||||
|
"""
|
||||||
|
Import `in_stream` to the :class:`Databook` object using the `format`.
|
||||||
|
`in_stream` can be a file-like object, a string, or a bytestring.
|
||||||
|
|
||||||
|
:param \\*\\*kwargs: (optional) custom configuration to the format `import_book`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stream = normalize_input(in_stream)
|
||||||
|
if not format:
|
||||||
|
format = detect_format(stream)
|
||||||
|
|
||||||
|
fmt = registry.get_format(format)
|
||||||
|
if not hasattr(fmt, 'import_book'):
|
||||||
|
raise UnsupportedFormat(f'Format {format} cannot be loaded.')
|
||||||
|
|
||||||
|
fmt.import_book(self, stream, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self, format, **kwargs):
|
||||||
|
"""
|
||||||
|
Export :class:`Databook` object to `format`.
|
||||||
|
|
||||||
|
:param \\*\\*kwargs: (optional) custom configuration to the format `export_book`.
|
||||||
|
"""
|
||||||
|
fmt = registry.get_format(format)
|
||||||
|
if not hasattr(fmt, 'export_book'):
|
||||||
|
raise UnsupportedFormat(f'Format {format} cannot be exported.')
|
||||||
|
|
||||||
|
return fmt.export_book(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_format(stream):
|
||||||
|
"""Return format name of given stream (file-like object, string, or bytestring)."""
|
||||||
|
stream = normalize_input(stream)
|
||||||
|
fmt_title = None
|
||||||
|
for fmt in registry.formats():
|
||||||
|
try:
|
||||||
|
if fmt.detect(stream):
|
||||||
|
fmt_title = fmt.title
|
||||||
|
break
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if hasattr(stream, 'seek'):
|
||||||
|
stream.seek(0)
|
||||||
|
return fmt_title
|
||||||
|
|
||||||
|
|
||||||
|
def import_set(stream, format=None, **kwargs):
|
||||||
|
"""Return dataset of given stream (file-like object, string, or bytestring)."""
|
||||||
|
|
||||||
|
return Dataset().load(normalize_input(stream), format, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def import_book(stream, format=None, **kwargs):
|
||||||
|
"""Return dataset of given stream (file-like object, string, or bytestring)."""
|
||||||
|
|
||||||
|
return Databook().load(normalize_input(stream), format, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
registry.register_builtins()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
class InvalidDatasetType(Exception):
|
||||||
|
"Only Datasets can be added to a DataBook"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDimensions(Exception):
|
||||||
|
"Invalid size"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDatasetIndex(Exception):
|
||||||
|
"Outside of Dataset size"
|
||||||
|
|
||||||
|
|
||||||
|
class HeadersNeeded(Exception):
|
||||||
|
"Header parameter must be given when appending a column in this Dataset."
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedFormat(NotImplementedError):
|
||||||
|
"Format is not supported"
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
""" Tablib - formats
|
||||||
|
"""
|
||||||
|
from collections import OrderedDict
|
||||||
|
from functools import partialmethod
|
||||||
|
from importlib import import_module
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
|
from tablib.exceptions import UnsupportedFormat
|
||||||
|
from tablib.utils import normalize_input
|
||||||
|
|
||||||
|
from ._csv import CSVFormat
|
||||||
|
from ._json import JSONFormat
|
||||||
|
from ._tsv import TSVFormat
|
||||||
|
|
||||||
|
uninstalled_format_messages = {
|
||||||
|
"cli": {"package_name": "tabulate package", "extras_name": "cli"},
|
||||||
|
"df": {"package_name": "pandas package", "extras_name": "pandas"},
|
||||||
|
"html": {"package_name": "MarkupPy package", "extras_name": "html"},
|
||||||
|
"ods": {"package_name": "odfpy package", "extras_name": "ods"},
|
||||||
|
"xls": {"package_name": "xlrd and xlwt packages", "extras_name": "xls"},
|
||||||
|
"xlsx": {"package_name": "openpyxl package", "extras_name": "xlsx"},
|
||||||
|
"yaml": {"package_name": "pyyaml package", "extras_name": "yaml"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_format_class(dotted_path):
|
||||||
|
try:
|
||||||
|
module_path, class_name = dotted_path.rsplit('.', 1)
|
||||||
|
return getattr(import_module(module_path), class_name)
|
||||||
|
except (ValueError, AttributeError) as err:
|
||||||
|
raise ImportError(f"Unable to load format class '{dotted_path}' ({err})")
|
||||||
|
|
||||||
|
|
||||||
|
class FormatDescriptorBase:
|
||||||
|
def __init__(self, key, format_or_path):
|
||||||
|
self.key = key
|
||||||
|
self._format_path = None
|
||||||
|
if isinstance(format_or_path, str):
|
||||||
|
self._format = None
|
||||||
|
self._format_path = format_or_path
|
||||||
|
else:
|
||||||
|
self._format = format_or_path
|
||||||
|
|
||||||
|
def ensure_format_loaded(self):
|
||||||
|
if self._format is None:
|
||||||
|
self._format = load_format_class(self._format_path)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportExportBookDescriptor(FormatDescriptorBase):
|
||||||
|
def __get__(self, obj, cls, **kwargs):
|
||||||
|
self.ensure_format_loaded()
|
||||||
|
return self._format.export_book(obj, **kwargs)
|
||||||
|
|
||||||
|
def __set__(self, obj, val):
|
||||||
|
self.ensure_format_loaded()
|
||||||
|
return self._format.import_book(obj, normalize_input(val))
|
||||||
|
|
||||||
|
|
||||||
|
class ImportExportSetDescriptor(FormatDescriptorBase):
|
||||||
|
def __get__(self, obj, cls, **kwargs):
|
||||||
|
self.ensure_format_loaded()
|
||||||
|
return self._format.export_set(obj, **kwargs)
|
||||||
|
|
||||||
|
def __set__(self, obj, val):
|
||||||
|
self.ensure_format_loaded()
|
||||||
|
return self._format.import_set(obj, normalize_input(val))
|
||||||
|
|
||||||
|
|
||||||
|
class Registry:
|
||||||
|
_formats = OrderedDict()
|
||||||
|
|
||||||
|
def register(self, key, format_or_path):
|
||||||
|
from tablib.core import Databook, Dataset
|
||||||
|
|
||||||
|
# Create Databook.<format> read or read/write properties
|
||||||
|
setattr(Databook, key, ImportExportBookDescriptor(key, format_or_path))
|
||||||
|
|
||||||
|
# Create Dataset.<format> read or read/write properties,
|
||||||
|
# and Dataset.get_<format>/set_<format> methods.
|
||||||
|
setattr(Dataset, key, ImportExportSetDescriptor(key, format_or_path))
|
||||||
|
try:
|
||||||
|
setattr(Dataset, 'get_%s' % key, partialmethod(Dataset._get_in_format, key))
|
||||||
|
setattr(Dataset, 'set_%s' % key, partialmethod(Dataset._set_in_format, key))
|
||||||
|
except AttributeError:
|
||||||
|
setattr(Dataset, 'get_%s' % key, partialmethod(Dataset._get_in_format, key))
|
||||||
|
|
||||||
|
self._formats[key] = format_or_path
|
||||||
|
|
||||||
|
def register_builtins(self):
|
||||||
|
# Registration ordering matters for autodetection.
|
||||||
|
self.register('json', JSONFormat())
|
||||||
|
# xlsx before as xls (xlrd) can also read xlsx
|
||||||
|
if find_spec('openpyxl'):
|
||||||
|
self.register('xlsx', 'tablib.formats._xlsx.XLSXFormat')
|
||||||
|
if find_spec('xlrd') and find_spec('xlwt'):
|
||||||
|
self.register('xls', 'tablib.formats._xls.XLSFormat')
|
||||||
|
if find_spec('yaml'):
|
||||||
|
self.register('yaml', 'tablib.formats._yaml.YAMLFormat')
|
||||||
|
self.register('csv', CSVFormat())
|
||||||
|
self.register('tsv', TSVFormat())
|
||||||
|
if find_spec('odf'):
|
||||||
|
self.register('ods', 'tablib.formats._ods.ODSFormat')
|
||||||
|
self.register('dbf', 'tablib.formats._dbf.DBFFormat')
|
||||||
|
if find_spec('MarkupPy'):
|
||||||
|
self.register('html', 'tablib.formats._html.HTMLFormat')
|
||||||
|
self.register('jira', 'tablib.formats._jira.JIRAFormat')
|
||||||
|
self.register('latex', 'tablib.formats._latex.LATEXFormat')
|
||||||
|
if find_spec('pandas'):
|
||||||
|
self.register('df', 'tablib.formats._df.DataFrameFormat')
|
||||||
|
self.register('rst', 'tablib.formats._rst.ReSTFormat')
|
||||||
|
if find_spec('tabulate'):
|
||||||
|
self.register('cli', 'tablib.formats._cli.CLIFormat')
|
||||||
|
|
||||||
|
def formats(self):
|
||||||
|
for key, frm in self._formats.items():
|
||||||
|
if isinstance(frm, str):
|
||||||
|
self._formats[key] = load_format_class(frm)
|
||||||
|
yield self._formats[key]
|
||||||
|
|
||||||
|
def get_format(self, key):
|
||||||
|
if key not in self._formats:
|
||||||
|
if key in uninstalled_format_messages:
|
||||||
|
raise UnsupportedFormat(
|
||||||
|
"The '{key}' format is not available. You may want to install the "
|
||||||
|
"{package_name} (or `pip install \"tablib[{extras_name}]\"`).".format(
|
||||||
|
**uninstalled_format_messages[key], key=key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise UnsupportedFormat("Tablib has no format '%s' or it is not registered." % key)
|
||||||
|
if isinstance(self._formats[key], str):
|
||||||
|
self._formats[key] = load_format_class(self._formats[key])
|
||||||
|
return self._formats[key]
|
||||||
|
|
||||||
|
|
||||||
|
registry = Registry()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Tablib - Command-line Interface table export support.
|
||||||
|
|
||||||
|
Generates a representation for CLI from the dataset.
|
||||||
|
Wrapper for tabulate library.
|
||||||
|
"""
|
||||||
|
from tabulate import tabulate as Tabulate
|
||||||
|
|
||||||
|
|
||||||
|
class CLIFormat:
|
||||||
|
""" Class responsible to export to CLI Format """
|
||||||
|
title = 'cli'
|
||||||
|
DEFAULT_FMT = 'plain'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset, **kwargs):
|
||||||
|
"""Returns CLI representation of a Dataset."""
|
||||||
|
if dataset.headers:
|
||||||
|
kwargs.setdefault('headers', dataset.headers)
|
||||||
|
kwargs.setdefault('tablefmt', cls.DEFAULT_FMT)
|
||||||
|
return Tabulate(dataset, **kwargs)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
""" Tablib - *SV Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
class CSVFormat:
|
||||||
|
title = 'csv'
|
||||||
|
extensions = ('csv',)
|
||||||
|
|
||||||
|
DEFAULT_DELIMITER = ','
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_stream_set(cls, dataset, **kwargs):
|
||||||
|
"""Returns CSV representation of Dataset as file-like."""
|
||||||
|
stream = StringIO()
|
||||||
|
|
||||||
|
kwargs.setdefault('delimiter', cls.DEFAULT_DELIMITER)
|
||||||
|
|
||||||
|
_csv = csv.writer(stream, **kwargs)
|
||||||
|
|
||||||
|
for row in dataset._package(dicts=False):
|
||||||
|
_csv.writerow(row)
|
||||||
|
|
||||||
|
stream.seek(0)
|
||||||
|
return stream
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset, **kwargs):
|
||||||
|
"""Returns CSV representation of Dataset."""
|
||||||
|
stream = cls.export_stream_set(dataset, **kwargs)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream, headers=True, **kwargs):
|
||||||
|
"""Returns dataset from CSV stream."""
|
||||||
|
|
||||||
|
dset.wipe()
|
||||||
|
|
||||||
|
kwargs.setdefault('delimiter', cls.DEFAULT_DELIMITER)
|
||||||
|
|
||||||
|
rows = csv.reader(in_stream, **kwargs)
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
|
||||||
|
if (i == 0) and (headers):
|
||||||
|
dset.headers = row
|
||||||
|
elif row:
|
||||||
|
if i > 0 and len(row) < dset.width:
|
||||||
|
row += [''] * (dset.width - len(row))
|
||||||
|
dset.append(row)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream, delimiter=None):
|
||||||
|
"""Returns True if given stream is valid CSV."""
|
||||||
|
try:
|
||||||
|
csv.Sniffer().sniff(stream.read(1024), delimiters=delimiter or cls.DEFAULT_DELIMITER)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
""" Tablib - DBF Support.
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from tablib.packages.dbfpy import dbf, dbfnew
|
||||||
|
from tablib.packages.dbfpy import record as dbfrecord
|
||||||
|
|
||||||
|
|
||||||
|
class DBFFormat:
|
||||||
|
title = 'dbf'
|
||||||
|
extensions = ('csv',)
|
||||||
|
|
||||||
|
DEFAULT_ENCODING = 'utf-8'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Returns DBF representation of a Dataset"""
|
||||||
|
new_dbf = dbfnew.dbf_new()
|
||||||
|
temp_file, temp_uri = tempfile.mkstemp()
|
||||||
|
|
||||||
|
# create the appropriate fields based on the contents of the first row
|
||||||
|
first_row = dataset[0]
|
||||||
|
for fieldname, field_value in zip(dataset.headers, first_row):
|
||||||
|
if type(field_value) in [int, float]:
|
||||||
|
new_dbf.add_field(fieldname, 'N', 10, 8)
|
||||||
|
else:
|
||||||
|
new_dbf.add_field(fieldname, 'C', 80)
|
||||||
|
|
||||||
|
new_dbf.write(temp_uri)
|
||||||
|
|
||||||
|
dbf_file = dbf.Dbf(temp_uri, readOnly=0)
|
||||||
|
for row in dataset:
|
||||||
|
record = dbfrecord.DbfRecord(dbf_file)
|
||||||
|
for fieldname, field_value in zip(dataset.headers, row):
|
||||||
|
record[fieldname] = field_value
|
||||||
|
record.store()
|
||||||
|
|
||||||
|
dbf_file.close()
|
||||||
|
dbf_stream = open(temp_uri, 'rb')
|
||||||
|
stream = io.BytesIO(dbf_stream.read())
|
||||||
|
dbf_stream.close()
|
||||||
|
os.close(temp_file)
|
||||||
|
os.remove(temp_uri)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream, headers=True):
|
||||||
|
"""Returns a dataset from a DBF stream."""
|
||||||
|
|
||||||
|
dset.wipe()
|
||||||
|
_dbf = dbf.Dbf(in_stream)
|
||||||
|
dset.headers = _dbf.fieldNames
|
||||||
|
for record in range(_dbf.recordCount):
|
||||||
|
row = [_dbf[record][f] for f in _dbf.fieldNames]
|
||||||
|
dset.append(row)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
"""Returns True if the given stream is valid DBF"""
|
||||||
|
try:
|
||||||
|
_dbf = dbf.Dbf(stream, readOnly=True)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
""" Tablib - DataFrame Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pandas import DataFrame
|
||||||
|
except ImportError:
|
||||||
|
DataFrame = None
|
||||||
|
|
||||||
|
|
||||||
|
class DataFrameFormat:
|
||||||
|
title = 'df'
|
||||||
|
extensions = ('df',)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
"""Returns True if given stream is a DataFrame."""
|
||||||
|
if DataFrame is None:
|
||||||
|
return False
|
||||||
|
elif isinstance(stream, DataFrame):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
DataFrame(stream.read())
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dset, index=None):
|
||||||
|
"""Returns DataFrame representation of DataBook."""
|
||||||
|
if DataFrame is None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'DataFrame Format requires `pandas` to be installed.'
|
||||||
|
' Try `pip install "tablib[pandas]"`.')
|
||||||
|
dataframe = DataFrame(dset.dict, columns=dset.headers)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream):
|
||||||
|
"""Returns dataset from DataFrame."""
|
||||||
|
dset.wipe()
|
||||||
|
dset.dict = in_stream.to_dict(orient='records')
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
""" Tablib - HTML export support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from MarkupPy import markup
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLFormat:
|
||||||
|
BOOK_ENDINGS = 'h3'
|
||||||
|
|
||||||
|
title = 'html'
|
||||||
|
extensions = ('html', )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""HTML representation of a Dataset."""
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
|
||||||
|
page = markup.page()
|
||||||
|
page.table.open()
|
||||||
|
|
||||||
|
if dataset.headers is not None:
|
||||||
|
new_header = [item if item is not None else '' for item in dataset.headers]
|
||||||
|
|
||||||
|
page.thead.open()
|
||||||
|
headers = markup.oneliner.th(new_header)
|
||||||
|
page.tr(headers)
|
||||||
|
page.thead.close()
|
||||||
|
|
||||||
|
for row in dataset:
|
||||||
|
new_row = [item if item is not None else '' for item in row]
|
||||||
|
|
||||||
|
html_row = markup.oneliner.td(new_row)
|
||||||
|
page.tr(html_row)
|
||||||
|
|
||||||
|
page.table.close()
|
||||||
|
|
||||||
|
# Allow unicode characters in output
|
||||||
|
wrapper = codecs.getwriter("utf8")(stream)
|
||||||
|
wrapper.writelines(str(page))
|
||||||
|
|
||||||
|
return stream.getvalue().decode('utf-8')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook):
|
||||||
|
"""HTML representation of a Databook."""
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
|
||||||
|
# Allow unicode characters in output
|
||||||
|
wrapper = codecs.getwriter("utf8")(stream)
|
||||||
|
|
||||||
|
for i, dset in enumerate(databook._datasets):
|
||||||
|
title = (dset.title if dset.title else 'Set %s' % (i))
|
||||||
|
wrapper.write(f'<{cls.BOOK_ENDINGS}>{title}</{cls.BOOK_ENDINGS}>\n')
|
||||||
|
wrapper.write(dset.html)
|
||||||
|
wrapper.write('\n')
|
||||||
|
|
||||||
|
return stream.getvalue().decode('utf-8')
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Tablib - Jira table export support.
|
||||||
|
|
||||||
|
Generates a Jira table from the dataset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class JIRAFormat:
|
||||||
|
title = 'jira'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Formats the dataset according to the Jira table syntax:
|
||||||
|
|
||||||
|
||heading 1||heading 2||heading 3||
|
||||||
|
|col A1|col A2|col A3|
|
||||||
|
|col B1|col B2|col B3|
|
||||||
|
|
||||||
|
:param dataset: dataset to serialize
|
||||||
|
:type dataset: tablib.core.Dataset
|
||||||
|
"""
|
||||||
|
|
||||||
|
header = cls._get_header(dataset.headers) if dataset.headers else ''
|
||||||
|
body = cls._get_body(dataset)
|
||||||
|
return f'{header}\n{body}' if header else body
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_body(cls, dataset):
|
||||||
|
return '\n'.join([cls._serialize_row(row) for row in dataset])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_header(cls, headers):
|
||||||
|
return cls._serialize_row(headers, delimiter='||')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _serialize_row(cls, row, delimiter='|'):
|
||||||
|
return '{}{}{}'.format(
|
||||||
|
delimiter,
|
||||||
|
delimiter.join([str(item) if item else ' ' for item in row]),
|
||||||
|
delimiter
|
||||||
|
)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
""" Tablib - JSON Support
|
||||||
|
"""
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import tablib
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_objects_handler(obj):
|
||||||
|
if isinstance(obj, (decimal.Decimal, UUID)):
|
||||||
|
return str(obj)
|
||||||
|
elif hasattr(obj, 'isoformat'):
|
||||||
|
return obj.isoformat()
|
||||||
|
else:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class JSONFormat:
|
||||||
|
title = 'json'
|
||||||
|
extensions = ('json', 'jsn')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Returns JSON representation of Dataset."""
|
||||||
|
return json.dumps(dataset.dict, default=serialize_objects_handler)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook):
|
||||||
|
"""Returns JSON representation of Databook."""
|
||||||
|
return json.dumps(databook._package(), default=serialize_objects_handler)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream):
|
||||||
|
"""Returns dataset from JSON stream."""
|
||||||
|
|
||||||
|
dset.wipe()
|
||||||
|
dset.dict = json.load(in_stream)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_book(cls, dbook, in_stream):
|
||||||
|
"""Returns databook from JSON stream."""
|
||||||
|
|
||||||
|
dbook.wipe()
|
||||||
|
for sheet in json.load(in_stream):
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.title = sheet['title']
|
||||||
|
data.dict = sheet['data']
|
||||||
|
dbook.add_sheet(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
"""Returns True if given stream is valid JSON."""
|
||||||
|
try:
|
||||||
|
json.load(stream)
|
||||||
|
return True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""Tablib - LaTeX table export support.
|
||||||
|
|
||||||
|
Generates a LaTeX booktabs-style table from the dataset.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class LATEXFormat:
|
||||||
|
title = 'latex'
|
||||||
|
extensions = ('tex',)
|
||||||
|
|
||||||
|
TABLE_TEMPLATE = """\
|
||||||
|
%% Note: add \\usepackage{booktabs} to your preamble
|
||||||
|
%%
|
||||||
|
\\begin{table}[!htbp]
|
||||||
|
\\centering
|
||||||
|
%(CAPTION)s
|
||||||
|
\\begin{tabular}{%(COLSPEC)s}
|
||||||
|
\\toprule
|
||||||
|
%(HEADER)s
|
||||||
|
%(MIDRULE)s
|
||||||
|
%(BODY)s
|
||||||
|
\\bottomrule
|
||||||
|
\\end{tabular}
|
||||||
|
\\end{table}
|
||||||
|
"""
|
||||||
|
|
||||||
|
TEX_RESERVED_SYMBOLS_MAP = dict([
|
||||||
|
('\\', '\\textbackslash{}'),
|
||||||
|
('{', '\\{'),
|
||||||
|
('}', '\\}'),
|
||||||
|
('$', '\\$'),
|
||||||
|
('&', '\\&'),
|
||||||
|
('#', '\\#'),
|
||||||
|
('^', '\\textasciicircum{}'),
|
||||||
|
('_', '\\_'),
|
||||||
|
('~', '\\textasciitilde{}'),
|
||||||
|
('%', '\\%'),
|
||||||
|
])
|
||||||
|
|
||||||
|
TEX_RESERVED_SYMBOLS_RE = re.compile(
|
||||||
|
'(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys())))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Returns LaTeX representation of dataset
|
||||||
|
|
||||||
|
:param dataset: dataset to serialize
|
||||||
|
:type dataset: tablib.core.Dataset
|
||||||
|
"""
|
||||||
|
|
||||||
|
caption = '\\caption{%s}' % dataset.title if dataset.title else '%'
|
||||||
|
colspec = cls._colspec(dataset.width)
|
||||||
|
header = cls._serialize_row(dataset.headers) if dataset.headers else ''
|
||||||
|
midrule = cls._midrule(dataset.width)
|
||||||
|
body = '\n'.join([cls._serialize_row(row) for row in dataset])
|
||||||
|
return cls.TABLE_TEMPLATE % dict(CAPTION=caption, COLSPEC=colspec,
|
||||||
|
HEADER=header, MIDRULE=midrule, BODY=body)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _colspec(cls, dataset_width):
|
||||||
|
"""Generates the column specification for the LaTeX `tabular` environment
|
||||||
|
based on the dataset width.
|
||||||
|
|
||||||
|
The first column is justified to the left, all further columns are aligned
|
||||||
|
to the right.
|
||||||
|
|
||||||
|
.. note:: This is only a heuristic and most probably has to be fine-tuned
|
||||||
|
post export. Column alignment should depend on the data type, e.g., textual
|
||||||
|
content should usually be aligned to the left while numeric content almost
|
||||||
|
always should be aligned to the right.
|
||||||
|
|
||||||
|
:param dataset_width: width of the dataset
|
||||||
|
"""
|
||||||
|
|
||||||
|
spec = 'l'
|
||||||
|
for _ in range(1, dataset_width):
|
||||||
|
spec += 'r'
|
||||||
|
return spec
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _midrule(cls, dataset_width):
|
||||||
|
"""Generates the table `midrule`, which may be composed of several
|
||||||
|
`cmidrules`.
|
||||||
|
|
||||||
|
:param dataset_width: width of the dataset to serialize
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not dataset_width or dataset_width == 1:
|
||||||
|
return '\\midrule'
|
||||||
|
return ' '.join([cls._cmidrule(colindex, dataset_width) for colindex in
|
||||||
|
range(1, dataset_width + 1)])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cmidrule(cls, colindex, dataset_width):
|
||||||
|
"""Generates the `cmidrule` for a single column with appropriate trimming
|
||||||
|
based on the column position.
|
||||||
|
|
||||||
|
:param colindex: Column index
|
||||||
|
:param dataset_width: width of the dataset
|
||||||
|
"""
|
||||||
|
|
||||||
|
rule = '\\cmidrule(%s){%d-%d}'
|
||||||
|
if colindex == 1:
|
||||||
|
# Rule of first column is trimmed on the right
|
||||||
|
return rule % ('r', colindex, colindex)
|
||||||
|
if colindex == dataset_width:
|
||||||
|
# Rule of last column is trimmed on the left
|
||||||
|
return rule % ('l', colindex, colindex)
|
||||||
|
# Inner columns are trimmed on the left and right
|
||||||
|
return rule % ('lr', colindex, colindex)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _serialize_row(cls, row):
|
||||||
|
"""Returns string representation of a single row.
|
||||||
|
|
||||||
|
:param row: single dataset row
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_row = [cls._escape_tex_reserved_symbols(str(item)) if item else ''
|
||||||
|
for item in row]
|
||||||
|
return 6 * ' ' + ' & '.join(new_row) + ' \\\\'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _escape_tex_reserved_symbols(cls, input):
|
||||||
|
"""Escapes all TeX reserved symbols ('_', '~', etc.) in a string.
|
||||||
|
|
||||||
|
:param input: String to escape
|
||||||
|
"""
|
||||||
|
def replace(match):
|
||||||
|
return cls.TEX_RESERVED_SYMBOLS_MAP[match.group()]
|
||||||
|
return cls.TEX_RESERVED_SYMBOLS_RE.sub(replace, input)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
""" Tablib - ODF Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from odf import opendocument, style, table, text
|
||||||
|
|
||||||
|
bold = style.Style(name="bold", family="paragraph")
|
||||||
|
bold.addElement(style.TextProperties(fontweight="bold", fontweightasian="bold", fontweightcomplex="bold"))
|
||||||
|
|
||||||
|
|
||||||
|
class ODSFormat:
|
||||||
|
title = 'ods'
|
||||||
|
extensions = ('ods',)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Returns ODF representation of Dataset."""
|
||||||
|
|
||||||
|
wb = opendocument.OpenDocumentSpreadsheet()
|
||||||
|
wb.automaticstyles.addElement(bold)
|
||||||
|
|
||||||
|
ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset')
|
||||||
|
wb.spreadsheet.addElement(ws)
|
||||||
|
cls.dset_sheet(dataset, ws)
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook):
|
||||||
|
"""Returns ODF representation of DataBook."""
|
||||||
|
|
||||||
|
wb = opendocument.OpenDocumentSpreadsheet()
|
||||||
|
wb.automaticstyles.addElement(bold)
|
||||||
|
|
||||||
|
for i, dset in enumerate(databook._datasets):
|
||||||
|
ws = table.Table(name=dset.title if dset.title else 'Sheet%s' % (i))
|
||||||
|
wb.spreadsheet.addElement(ws)
|
||||||
|
cls.dset_sheet(dset, ws)
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dset_sheet(cls, dataset, ws):
|
||||||
|
"""Completes given worksheet from given Dataset."""
|
||||||
|
_package = dataset._package(dicts=False)
|
||||||
|
|
||||||
|
for i, sep in enumerate(dataset._separators):
|
||||||
|
_offset = i
|
||||||
|
_package.insert((sep[0] + _offset), (sep[1],))
|
||||||
|
|
||||||
|
for i, row in enumerate(_package):
|
||||||
|
row_number = i + 1
|
||||||
|
odf_row = table.TableRow(stylename=bold, defaultcellstylename='bold')
|
||||||
|
for j, col in enumerate(row):
|
||||||
|
try:
|
||||||
|
col = str(col, errors='ignore')
|
||||||
|
except TypeError:
|
||||||
|
# col is already str
|
||||||
|
pass
|
||||||
|
ws.addElement(table.TableColumn())
|
||||||
|
|
||||||
|
# bold headers
|
||||||
|
if (row_number == 1) and dataset.headers:
|
||||||
|
odf_row.setAttribute('stylename', bold)
|
||||||
|
ws.addElement(odf_row)
|
||||||
|
cell = table.TableCell()
|
||||||
|
p = text.P()
|
||||||
|
p.addElement(text.Span(text=col, stylename=bold))
|
||||||
|
cell.addElement(p)
|
||||||
|
odf_row.addElement(cell)
|
||||||
|
|
||||||
|
# wrap the rest
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if '\n' in col:
|
||||||
|
ws.addElement(odf_row)
|
||||||
|
cell = table.TableCell()
|
||||||
|
cell.addElement(text.P(text=col))
|
||||||
|
odf_row.addElement(cell)
|
||||||
|
else:
|
||||||
|
ws.addElement(odf_row)
|
||||||
|
cell = table.TableCell()
|
||||||
|
cell.addElement(text.P(text=col))
|
||||||
|
odf_row.addElement(cell)
|
||||||
|
except TypeError:
|
||||||
|
ws.addElement(odf_row)
|
||||||
|
cell = table.TableCell()
|
||||||
|
cell.addElement(text.P(text=col))
|
||||||
|
odf_row.addElement(cell)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
if isinstance(stream, bytes):
|
||||||
|
# load expects a file-like object.
|
||||||
|
stream = BytesIO(stream)
|
||||||
|
try:
|
||||||
|
opendocument.load(stream)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
""" Tablib - reStructuredText Support
|
||||||
|
"""
|
||||||
|
|
||||||
|
from itertools import zip_longest
|
||||||
|
from statistics import median
|
||||||
|
from textwrap import TextWrapper
|
||||||
|
|
||||||
|
JUSTIFY_LEFT = 'left'
|
||||||
|
JUSTIFY_CENTER = 'center'
|
||||||
|
JUSTIFY_RIGHT = 'right'
|
||||||
|
JUSTIFY_VALUES = (JUSTIFY_LEFT, JUSTIFY_CENTER, JUSTIFY_RIGHT)
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(value):
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
return value.decode('utf-8')
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _max_word_len(text):
|
||||||
|
"""
|
||||||
|
Return the length of the longest word in `text`.
|
||||||
|
|
||||||
|
>>> _max_word_len('Python Module for Tabular Datasets')
|
||||||
|
8
|
||||||
|
"""
|
||||||
|
return max([len(word) for word in text.split()], default=0) if text else 0
|
||||||
|
|
||||||
|
|
||||||
|
class ReSTFormat:
|
||||||
|
title = 'rst'
|
||||||
|
extensions = ('rst',)
|
||||||
|
|
||||||
|
MAX_TABLE_WIDTH = 80 # Roughly. It may be wider to avoid breaking words.
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_column_string_lengths(cls, dataset):
|
||||||
|
"""
|
||||||
|
Returns a list of string lengths of each column, and a list of
|
||||||
|
maximum word lengths.
|
||||||
|
"""
|
||||||
|
if dataset.headers:
|
||||||
|
column_lengths = [[len(h)] for h in dataset.headers]
|
||||||
|
word_lens = [_max_word_len(h) for h in dataset.headers]
|
||||||
|
else:
|
||||||
|
column_lengths = [[] for _ in range(dataset.width)]
|
||||||
|
word_lens = [0 for _ in range(dataset.width)]
|
||||||
|
for row in dataset.dict:
|
||||||
|
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||||
|
for i, val in enumerate(values):
|
||||||
|
text = to_str(val)
|
||||||
|
column_lengths[i].append(len(text))
|
||||||
|
word_lens[i] = max(word_lens[i], _max_word_len(text))
|
||||||
|
return column_lengths, word_lens
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _row_to_lines(cls, values, widths, wrapper, sep='|', justify=JUSTIFY_LEFT):
|
||||||
|
"""
|
||||||
|
Returns a table row of wrapped values as a list of lines
|
||||||
|
"""
|
||||||
|
if justify not in JUSTIFY_VALUES:
|
||||||
|
raise ValueError('Value of "justify" must be one of "{}"'.format(
|
||||||
|
'", "'.join(JUSTIFY_VALUES)
|
||||||
|
))
|
||||||
|
if justify == JUSTIFY_LEFT:
|
||||||
|
just = lambda text, width: text.ljust(width)
|
||||||
|
elif justify == JUSTIFY_CENTER:
|
||||||
|
just = lambda text, width: text.center(width)
|
||||||
|
else:
|
||||||
|
just = lambda text, width: text.rjust(width)
|
||||||
|
lpad = sep + ' ' if sep else ''
|
||||||
|
rpad = ' ' + sep if sep else ''
|
||||||
|
pad = ' ' + sep + ' '
|
||||||
|
cells = []
|
||||||
|
for value, width in zip(values, widths):
|
||||||
|
wrapper.width = width
|
||||||
|
text = to_str(value)
|
||||||
|
cell = wrapper.wrap(text)
|
||||||
|
cells.append(cell)
|
||||||
|
lines = zip_longest(*cells, fillvalue='')
|
||||||
|
lines = (
|
||||||
|
(just(cell_line, widths[i]) for i, cell_line in enumerate(line))
|
||||||
|
for line in lines
|
||||||
|
)
|
||||||
|
lines = [''.join((lpad, pad.join(line), rpad)) for line in lines]
|
||||||
|
return lines
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_column_widths(cls, dataset, max_table_width=MAX_TABLE_WIDTH, pad_len=3):
|
||||||
|
"""
|
||||||
|
Returns a list of column widths proportional to the median length
|
||||||
|
of the text in their cells.
|
||||||
|
"""
|
||||||
|
str_lens, word_lens = cls._get_column_string_lengths(dataset)
|
||||||
|
median_lens = [int(median(lens)) for lens in str_lens]
|
||||||
|
total = sum(median_lens)
|
||||||
|
if total > max_table_width - (pad_len * len(median_lens)):
|
||||||
|
column_widths = (max_table_width * l // total for l in median_lens)
|
||||||
|
else:
|
||||||
|
column_widths = (l for l in median_lens)
|
||||||
|
# Allow for separator and padding:
|
||||||
|
column_widths = (w - pad_len if w > pad_len else w for w in column_widths)
|
||||||
|
# Rather widen table than break words:
|
||||||
|
column_widths = [max(w, l) for w, l in zip(column_widths, word_lens)]
|
||||||
|
return column_widths
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set_as_simple_table(cls, dataset, column_widths=None):
|
||||||
|
"""
|
||||||
|
Returns reStructuredText grid table representation of dataset.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
wrapper = TextWrapper()
|
||||||
|
if column_widths is None:
|
||||||
|
column_widths = cls._get_column_widths(dataset, pad_len=2)
|
||||||
|
border = ' '.join(['=' * w for w in column_widths])
|
||||||
|
|
||||||
|
lines.append(border)
|
||||||
|
if dataset.headers:
|
||||||
|
lines.extend(cls._row_to_lines(
|
||||||
|
dataset.headers,
|
||||||
|
column_widths,
|
||||||
|
wrapper,
|
||||||
|
sep='',
|
||||||
|
justify=JUSTIFY_CENTER,
|
||||||
|
))
|
||||||
|
lines.append(border)
|
||||||
|
for row in dataset.dict:
|
||||||
|
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||||
|
lines.extend(cls._row_to_lines(values, column_widths, wrapper, ''))
|
||||||
|
lines.append(border)
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set_as_grid_table(cls, dataset, column_widths=None):
|
||||||
|
"""
|
||||||
|
Returns reStructuredText grid table representation of dataset.
|
||||||
|
|
||||||
|
|
||||||
|
>>> from tablib import Dataset
|
||||||
|
>>> from tablib.formats import registry
|
||||||
|
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||||
|
>>> data = Dataset()
|
||||||
|
>>> data.headers = ['A', 'B', 'A and B']
|
||||||
|
>>> for a, b in bits:
|
||||||
|
... data.append([bool(a), bool(b), bool(a * b)])
|
||||||
|
>>> rst = registry.get_format('rst')
|
||||||
|
>>> print(rst.export_set(data, force_grid=True))
|
||||||
|
+-------+-------+-------+
|
||||||
|
| A | B | A and |
|
||||||
|
| | | B |
|
||||||
|
+=======+=======+=======+
|
||||||
|
| False | False | False |
|
||||||
|
+-------+-------+-------+
|
||||||
|
| True | False | False |
|
||||||
|
+-------+-------+-------+
|
||||||
|
| False | True | False |
|
||||||
|
+-------+-------+-------+
|
||||||
|
| True | True | True |
|
||||||
|
+-------+-------+-------+
|
||||||
|
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
wrapper = TextWrapper()
|
||||||
|
if column_widths is None:
|
||||||
|
column_widths = cls._get_column_widths(dataset)
|
||||||
|
header_sep = '+=' + '=+='.join(['=' * w for w in column_widths]) + '=+'
|
||||||
|
row_sep = '+-' + '-+-'.join(['-' * w for w in column_widths]) + '-+'
|
||||||
|
|
||||||
|
lines.append(row_sep)
|
||||||
|
|
||||||
|
if dataset.headers:
|
||||||
|
lines.extend(cls._row_to_lines(
|
||||||
|
dataset.headers,
|
||||||
|
column_widths,
|
||||||
|
wrapper,
|
||||||
|
justify=JUSTIFY_CENTER,
|
||||||
|
))
|
||||||
|
lines.append(header_sep)
|
||||||
|
for row in dataset.dict:
|
||||||
|
values = iter(row.values() if hasattr(row, 'values') else row)
|
||||||
|
lines.extend(cls._row_to_lines(values, column_widths, wrapper))
|
||||||
|
lines.append(row_sep)
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _use_simple_table(cls, head0, col0, width0):
|
||||||
|
"""
|
||||||
|
Use a simple table if the text in the first column is never wrapped
|
||||||
|
|
||||||
|
|
||||||
|
>>> from tablib.formats import registry
|
||||||
|
>>> rst = registry.get_format('rst')
|
||||||
|
>>> rst._use_simple_table('menu', ['egg', 'bacon'], 10)
|
||||||
|
True
|
||||||
|
>>> rst._use_simple_table(None, ['lobster thermidor', 'spam'], 10)
|
||||||
|
False
|
||||||
|
|
||||||
|
"""
|
||||||
|
if head0 is not None:
|
||||||
|
head0 = to_str(head0)
|
||||||
|
if len(head0) > width0:
|
||||||
|
return False
|
||||||
|
for cell in col0:
|
||||||
|
cell = to_str(cell)
|
||||||
|
if len(cell) > width0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns reStructuredText table representation of dataset.
|
||||||
|
|
||||||
|
Returns a simple table if the text in the first column is never
|
||||||
|
wrapped, otherwise returns a grid table.
|
||||||
|
|
||||||
|
|
||||||
|
>>> from tablib import Dataset
|
||||||
|
>>> bits = ((0, 0), (1, 0), (0, 1), (1, 1))
|
||||||
|
>>> data = Dataset()
|
||||||
|
>>> data.headers = ['A', 'B', 'A and B']
|
||||||
|
>>> for a, b in bits:
|
||||||
|
... data.append([bool(a), bool(b), bool(a * b)])
|
||||||
|
>>> table = data.rst
|
||||||
|
>>> table.split('\\n') == [
|
||||||
|
... '===== ===== =====',
|
||||||
|
... ' A B A and',
|
||||||
|
... ' B ',
|
||||||
|
... '===== ===== =====',
|
||||||
|
... 'False False False',
|
||||||
|
... 'True False False',
|
||||||
|
... 'False True False',
|
||||||
|
... 'True True True ',
|
||||||
|
... '===== ===== =====',
|
||||||
|
... ]
|
||||||
|
True
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not dataset.dict:
|
||||||
|
return ''
|
||||||
|
force_grid = kwargs.get('force_grid', False)
|
||||||
|
max_table_width = kwargs.get('max_table_width', cls.MAX_TABLE_WIDTH)
|
||||||
|
column_widths = cls._get_column_widths(dataset, max_table_width)
|
||||||
|
|
||||||
|
use_simple_table = cls._use_simple_table(
|
||||||
|
dataset.headers[0] if dataset.headers else None,
|
||||||
|
dataset.get_col(0),
|
||||||
|
column_widths[0],
|
||||||
|
)
|
||||||
|
if use_simple_table and not force_grid:
|
||||||
|
return cls.export_set_as_simple_table(dataset, column_widths)
|
||||||
|
else:
|
||||||
|
return cls.export_set_as_grid_table(dataset, column_widths)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook):
|
||||||
|
"""
|
||||||
|
reStructuredText representation of a Databook.
|
||||||
|
|
||||||
|
Tables are separated by a blank line. All tables use the grid
|
||||||
|
format.
|
||||||
|
"""
|
||||||
|
return '\n\n'.join(cls.export_set(dataset, force_grid=True)
|
||||||
|
for dataset in databook._datasets)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
""" Tablib - TSV (Tab Separated Values) Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._csv import CSVFormat
|
||||||
|
|
||||||
|
|
||||||
|
class TSVFormat(CSVFormat):
|
||||||
|
title = 'tsv'
|
||||||
|
extensions = ('tsv',)
|
||||||
|
|
||||||
|
DEFAULT_DELIMITER = '\t'
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
""" Tablib - XLS Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import xlrd
|
||||||
|
import xlwt
|
||||||
|
from xlrd.xldate import xldate_as_datetime
|
||||||
|
|
||||||
|
import tablib
|
||||||
|
|
||||||
|
# special styles
|
||||||
|
wrap = xlwt.easyxf("alignment: wrap on")
|
||||||
|
bold = xlwt.easyxf("font: bold on")
|
||||||
|
|
||||||
|
|
||||||
|
class XLSFormat:
|
||||||
|
title = 'xls'
|
||||||
|
extensions = ('xls',)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
"""Returns True if given stream is a readable excel file."""
|
||||||
|
try:
|
||||||
|
xlrd.open_workbook(file_contents=stream)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
xlrd.open_workbook(file_contents=stream.read())
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
xlrd.open_workbook(filename=stream)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Returns XLS representation of Dataset."""
|
||||||
|
|
||||||
|
wb = xlwt.Workbook(encoding='utf8')
|
||||||
|
ws = wb.add_sheet(dataset.title if dataset.title else 'Tablib Dataset')
|
||||||
|
|
||||||
|
cls.dset_sheet(dataset, ws)
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook):
|
||||||
|
"""Returns XLS representation of DataBook."""
|
||||||
|
|
||||||
|
wb = xlwt.Workbook(encoding='utf8')
|
||||||
|
|
||||||
|
for i, dset in enumerate(databook._datasets):
|
||||||
|
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
|
||||||
|
|
||||||
|
cls.dset_sheet(dset, ws)
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream, headers=True):
|
||||||
|
"""Returns databook from XLS stream."""
|
||||||
|
|
||||||
|
dset.wipe()
|
||||||
|
|
||||||
|
xls_book = xlrd.open_workbook(file_contents=in_stream.read())
|
||||||
|
sheet = xls_book.sheet_by_index(0)
|
||||||
|
|
||||||
|
dset.title = sheet.name
|
||||||
|
|
||||||
|
def cell_value(value, type_):
|
||||||
|
if type_ == xlrd.XL_CELL_ERROR:
|
||||||
|
return xlrd.error_text_from_code[value]
|
||||||
|
elif type_ == xlrd.XL_CELL_DATE:
|
||||||
|
return xldate_as_datetime(value, xls_book.datemode)
|
||||||
|
return value
|
||||||
|
|
||||||
|
for i in range(sheet.nrows):
|
||||||
|
if i == 0 and headers:
|
||||||
|
dset.headers = sheet.row_values(0)
|
||||||
|
else:
|
||||||
|
dset.append([
|
||||||
|
cell_value(val, typ)
|
||||||
|
for val, typ in zip(sheet.row_values(i), sheet.row_types(i))
|
||||||
|
])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_book(cls, dbook, in_stream, headers=True):
|
||||||
|
"""Returns databook from XLS stream."""
|
||||||
|
|
||||||
|
dbook.wipe()
|
||||||
|
|
||||||
|
xls_book = xlrd.open_workbook(file_contents=in_stream)
|
||||||
|
|
||||||
|
for sheet in xls_book.sheets():
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.title = sheet.name
|
||||||
|
|
||||||
|
for i in range(sheet.nrows):
|
||||||
|
if i == 0 and headers:
|
||||||
|
data.headers = sheet.row_values(0)
|
||||||
|
else:
|
||||||
|
data.append(sheet.row_values(i))
|
||||||
|
|
||||||
|
dbook.add_sheet(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dset_sheet(cls, dataset, ws):
|
||||||
|
"""Completes given worksheet from given Dataset."""
|
||||||
|
_package = dataset._package(dicts=False)
|
||||||
|
|
||||||
|
for i, sep in enumerate(dataset._separators):
|
||||||
|
_offset = i
|
||||||
|
_package.insert((sep[0] + _offset), (sep[1],))
|
||||||
|
|
||||||
|
for i, row in enumerate(_package):
|
||||||
|
for j, col in enumerate(row):
|
||||||
|
|
||||||
|
# bold headers
|
||||||
|
if (i == 0) and dataset.headers:
|
||||||
|
ws.write(i, j, col, bold)
|
||||||
|
|
||||||
|
# frozen header row
|
||||||
|
ws.panes_frozen = True
|
||||||
|
ws.horz_split_pos = 1
|
||||||
|
|
||||||
|
# bold separators
|
||||||
|
elif len(row) < dataset.width:
|
||||||
|
ws.write(i, j, col, bold)
|
||||||
|
|
||||||
|
# wrap the rest
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if '\n' in col:
|
||||||
|
ws.write(i, j, col, wrap)
|
||||||
|
else:
|
||||||
|
ws.write(i, j, col)
|
||||||
|
except TypeError:
|
||||||
|
ws.write(i, j, col)
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
""" Tablib - XLSX Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from openpyxl.reader.excel import ExcelReader, load_workbook
|
||||||
|
from openpyxl.styles import Alignment, Font
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from openpyxl.workbook import Workbook
|
||||||
|
from openpyxl.writer.excel import ExcelWriter
|
||||||
|
|
||||||
|
import tablib
|
||||||
|
|
||||||
|
|
||||||
|
class XLSXFormat:
|
||||||
|
title = 'xlsx'
|
||||||
|
extensions = ('xlsx',)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
"""Returns True if given stream is a readable excel file."""
|
||||||
|
try:
|
||||||
|
# No need to fully load the file, it should be enough to be able to
|
||||||
|
# read the manifest.
|
||||||
|
reader = ExcelReader(stream, read_only=False)
|
||||||
|
reader.read_manifest()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset, freeze_panes=True):
|
||||||
|
"""Returns XLSX representation of Dataset."""
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.worksheets[0]
|
||||||
|
ws.title = dataset.title if dataset.title else 'Tablib Dataset'
|
||||||
|
|
||||||
|
cls.dset_sheet(dataset, ws, freeze_panes=freeze_panes)
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook, freeze_panes=True):
|
||||||
|
"""Returns XLSX representation of DataBook."""
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
wb.remove(sheet)
|
||||||
|
for i, dset in enumerate(databook._datasets):
|
||||||
|
ws = wb.create_sheet()
|
||||||
|
ws.title = dset.title if dset.title else 'Sheet%s' % (i)
|
||||||
|
|
||||||
|
cls.dset_sheet(dset, ws, freeze_panes=freeze_panes)
|
||||||
|
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream, headers=True, read_only=True):
|
||||||
|
"""Returns databook from XLS stream."""
|
||||||
|
|
||||||
|
dset.wipe()
|
||||||
|
|
||||||
|
xls_book = load_workbook(in_stream, read_only=read_only, data_only=True)
|
||||||
|
sheet = xls_book.active
|
||||||
|
|
||||||
|
dset.title = sheet.title
|
||||||
|
|
||||||
|
for i, row in enumerate(sheet.rows):
|
||||||
|
row_vals = [c.value for c in row]
|
||||||
|
if (i == 0) and (headers):
|
||||||
|
dset.headers = row_vals
|
||||||
|
else:
|
||||||
|
dset.append(row_vals)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_book(cls, dbook, in_stream, headers=True, read_only=True):
|
||||||
|
"""Returns databook from XLS stream."""
|
||||||
|
|
||||||
|
dbook.wipe()
|
||||||
|
|
||||||
|
xls_book = load_workbook(in_stream, read_only=read_only, data_only=True)
|
||||||
|
|
||||||
|
for sheet in xls_book.worksheets:
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.title = sheet.title
|
||||||
|
|
||||||
|
for i, row in enumerate(sheet.rows):
|
||||||
|
row_vals = [c.value for c in row]
|
||||||
|
if (i == 0) and (headers):
|
||||||
|
data.headers = row_vals
|
||||||
|
else:
|
||||||
|
if i > 0 and len(row_vals) < data.width:
|
||||||
|
row_vals += [''] * (data.width - len(row_vals))
|
||||||
|
data.append(row_vals)
|
||||||
|
|
||||||
|
dbook.add_sheet(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dset_sheet(cls, dataset, ws, freeze_panes=True):
|
||||||
|
"""Completes given worksheet from given Dataset."""
|
||||||
|
_package = dataset._package(dicts=False)
|
||||||
|
|
||||||
|
for i, sep in enumerate(dataset._separators):
|
||||||
|
_offset = i
|
||||||
|
_package.insert((sep[0] + _offset), (sep[1],))
|
||||||
|
|
||||||
|
bold = Font(bold=True)
|
||||||
|
wrap_text = Alignment(wrap_text=True)
|
||||||
|
|
||||||
|
for i, row in enumerate(_package):
|
||||||
|
row_number = i + 1
|
||||||
|
for j, col in enumerate(row):
|
||||||
|
col_idx = get_column_letter(j + 1)
|
||||||
|
cell = ws[f'{col_idx}{row_number}']
|
||||||
|
|
||||||
|
# bold headers
|
||||||
|
if (row_number == 1) and dataset.headers:
|
||||||
|
cell.font = bold
|
||||||
|
if freeze_panes:
|
||||||
|
# Export Freeze only after first Line
|
||||||
|
ws.freeze_panes = 'A2'
|
||||||
|
|
||||||
|
# bold separators
|
||||||
|
elif len(row) < dataset.width:
|
||||||
|
cell.font = bold
|
||||||
|
|
||||||
|
# wrap the rest
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
str_col_value = str(col)
|
||||||
|
except TypeError:
|
||||||
|
str_col_value = ''
|
||||||
|
if '\n' in str_col_value:
|
||||||
|
cell.alignment = wrap_text
|
||||||
|
|
||||||
|
try:
|
||||||
|
cell.value = col
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
cell.value = str(col)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
""" Tablib - YAML Support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
import tablib
|
||||||
|
|
||||||
|
|
||||||
|
class YAMLFormat:
|
||||||
|
title = 'yaml'
|
||||||
|
extensions = ('yaml', 'yml')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_set(cls, dataset):
|
||||||
|
"""Returns YAML representation of Dataset."""
|
||||||
|
|
||||||
|
return yaml.safe_dump(dataset._package(ordered=False), default_flow_style=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_book(cls, databook):
|
||||||
|
"""Returns YAML representation of Databook."""
|
||||||
|
return yaml.safe_dump(databook._package(ordered=False), default_flow_style=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_set(cls, dset, in_stream):
|
||||||
|
"""Returns dataset from YAML stream."""
|
||||||
|
|
||||||
|
dset.wipe()
|
||||||
|
dset.dict = yaml.safe_load(in_stream)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_book(cls, dbook, in_stream):
|
||||||
|
"""Returns databook from YAML stream."""
|
||||||
|
|
||||||
|
dbook.wipe()
|
||||||
|
|
||||||
|
for sheet in yaml.safe_load(in_stream):
|
||||||
|
data = tablib.Dataset()
|
||||||
|
data.title = sheet['title']
|
||||||
|
data.dict = sheet['data']
|
||||||
|
dbook.add_sheet(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def detect(cls, stream):
|
||||||
|
"""Returns True if given stream is valid YAML."""
|
||||||
|
try:
|
||||||
|
_yaml = yaml.safe_load(stream)
|
||||||
|
if isinstance(_yaml, (list, tuple, dict)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except (yaml.parser.ParserError, yaml.reader.ReaderError,
|
||||||
|
yaml.scanner.ScannerError):
|
||||||
|
return False
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
"""DBF accessing helpers.
|
||||||
|
|
||||||
|
FIXME: more documentation needed
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Create new table, setup structure, add records:
|
||||||
|
|
||||||
|
dbf = Dbf(filename, new=True)
|
||||||
|
dbf.addField(
|
||||||
|
("NAME", "C", 15),
|
||||||
|
("SURNAME", "C", 25),
|
||||||
|
("INITIALS", "C", 10),
|
||||||
|
("BIRTHDATE", "D"),
|
||||||
|
)
|
||||||
|
for (n, s, i, b) in (
|
||||||
|
("John", "Miller", "YC", (1980, 10, 11)),
|
||||||
|
("Andy", "Larkin", "", (1980, 4, 11)),
|
||||||
|
):
|
||||||
|
rec = dbf.newRecord()
|
||||||
|
rec["NAME"] = n
|
||||||
|
rec["SURNAME"] = s
|
||||||
|
rec["INITIALS"] = i
|
||||||
|
rec["BIRTHDATE"] = b
|
||||||
|
rec.store()
|
||||||
|
dbf.close()
|
||||||
|
|
||||||
|
Open existed dbf, read some data:
|
||||||
|
|
||||||
|
dbf = Dbf(filename, True)
|
||||||
|
for rec in dbf:
|
||||||
|
for fldName in dbf.fieldNames:
|
||||||
|
print('%s:\t %s (%s)' % (fldName, rec[fldName],
|
||||||
|
type(rec[fldName])))
|
||||||
|
print()
|
||||||
|
dbf.close()
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""History (most recent first):
|
||||||
|
11-feb-2007 [als] export INVALID_VALUE;
|
||||||
|
Dbf: added .ignoreErrors, .INVALID_VALUE
|
||||||
|
04-jul-2006 [als] added export declaration
|
||||||
|
20-dec-2005 [yc] removed fromStream and newDbf methods:
|
||||||
|
use argument of __init__ call must be used instead;
|
||||||
|
added class fields pointing to the header and
|
||||||
|
record classes.
|
||||||
|
17-dec-2005 [yc] split to several modules; reimplemented
|
||||||
|
13-dec-2005 [yc] adapted to the changes of the `strutil` module.
|
||||||
|
13-sep-2002 [als] support FoxPro Timestamp datatype
|
||||||
|
15-nov-1999 [jjk] documentation updates, add demo
|
||||||
|
24-aug-1998 [jjk] add some encodeValue methods (not tested), other tweaks
|
||||||
|
08-jun-1998 [jjk] fix problems, add more features
|
||||||
|
20-feb-1998 [jjk] fix problems, add more features
|
||||||
|
19-feb-1998 [jjk] add create/write capabilities
|
||||||
|
18-feb-1998 [jjk] from dbfload.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||||
|
__date__ = "$Date: 2007/02/11 09:23:13 $"[7:-2]
|
||||||
|
__author__ = "Jeff Kunce <kuncej@mail.conservation.state.mo.us>"
|
||||||
|
|
||||||
|
__all__ = ["Dbf"]
|
||||||
|
|
||||||
|
from . import header, record
|
||||||
|
from .utils import INVALID_VALUE
|
||||||
|
|
||||||
|
|
||||||
|
class Dbf:
|
||||||
|
"""DBF accessor.
|
||||||
|
|
||||||
|
FIXME:
|
||||||
|
docs and examples needed (dont' forget to tell
|
||||||
|
about problems adding new fields on the fly)
|
||||||
|
|
||||||
|
Implementation notes:
|
||||||
|
``_new`` field is used to indicate whether this is
|
||||||
|
a new data table. `addField` could be used only for
|
||||||
|
the new tables! If at least one record was appended
|
||||||
|
to the table it's structure couldn't be changed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("name", "header", "stream",
|
||||||
|
"_changed", "_new", "_ignore_errors")
|
||||||
|
|
||||||
|
HeaderClass = header.DbfHeader
|
||||||
|
RecordClass = record.DbfRecord
|
||||||
|
INVALID_VALUE = INVALID_VALUE
|
||||||
|
|
||||||
|
# initialization and creation helpers
|
||||||
|
|
||||||
|
def __init__(self, f, readOnly=False, new=False, ignoreErrors=False):
|
||||||
|
"""Initialize instance.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
f:
|
||||||
|
Filename or file-like object.
|
||||||
|
new:
|
||||||
|
True if new data table must be created. Assume
|
||||||
|
data table exists if this argument is False.
|
||||||
|
readOnly:
|
||||||
|
if ``f`` argument is a string file will
|
||||||
|
be opend in read-only mode; in other cases
|
||||||
|
this argument is ignored. This argument is ignored
|
||||||
|
even if ``new`` argument is True.
|
||||||
|
headerObj:
|
||||||
|
`header.DbfHeader` instance or None. If this argument
|
||||||
|
is None, new empty header will be used with the
|
||||||
|
all fields set by default.
|
||||||
|
ignoreErrors:
|
||||||
|
if set, failing field value conversion will return
|
||||||
|
``INVALID_VALUE`` instead of raising conversion error.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(f, str):
|
||||||
|
# a filename
|
||||||
|
self.name = f
|
||||||
|
if new:
|
||||||
|
# new table (table file must be
|
||||||
|
# created or opened and truncated)
|
||||||
|
self.stream = open(f, "w+b")
|
||||||
|
else:
|
||||||
|
# table file must exist
|
||||||
|
self.stream = open(f, ("r+b", "rb")[bool(readOnly)])
|
||||||
|
else:
|
||||||
|
# a stream
|
||||||
|
self.name = getattr(f, "name", "")
|
||||||
|
self.stream = f
|
||||||
|
if new:
|
||||||
|
# if this is a new table, header will be empty
|
||||||
|
self.header = self.HeaderClass()
|
||||||
|
else:
|
||||||
|
# or instantiated using stream
|
||||||
|
self.header = self.HeaderClass.fromStream(self.stream)
|
||||||
|
self.ignoreErrors = ignoreErrors
|
||||||
|
self._new = bool(new)
|
||||||
|
self._changed = False
|
||||||
|
|
||||||
|
# properties
|
||||||
|
|
||||||
|
closed = property(lambda self: self.stream.closed)
|
||||||
|
recordCount = property(lambda self: self.header.recordCount)
|
||||||
|
fieldNames = property(
|
||||||
|
lambda self: [_fld.name for _fld in self.header.fields])
|
||||||
|
fieldDefs = property(lambda self: self.header.fields)
|
||||||
|
changed = property(lambda self: self._changed or self.header.changed)
|
||||||
|
|
||||||
|
def ignoreErrors(self, value):
|
||||||
|
"""Update `ignoreErrors` flag on the header object and self"""
|
||||||
|
self.header.ignoreErrors = self._ignore_errors = bool(value)
|
||||||
|
|
||||||
|
ignoreErrors = property(
|
||||||
|
lambda self: self._ignore_errors,
|
||||||
|
ignoreErrors,
|
||||||
|
doc="""Error processing mode for DBF field value conversion
|
||||||
|
|
||||||
|
if set, failing field value conversion will return
|
||||||
|
``INVALID_VALUE`` instead of raising conversion error.
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
# protected methods
|
||||||
|
|
||||||
|
def _fixIndex(self, index):
|
||||||
|
"""Return fixed index.
|
||||||
|
|
||||||
|
This method fails if index isn't a numeric object
|
||||||
|
(long or int). Or index isn't in a valid range
|
||||||
|
(less or equal to the number of records in the db).
|
||||||
|
|
||||||
|
If ``index`` is a negative number, it will be
|
||||||
|
treated as a negative indexes for list objects.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is numeric object maning valid index.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(index, int):
|
||||||
|
raise TypeError("Index must be a numeric object")
|
||||||
|
if index < 0:
|
||||||
|
# index from the right side
|
||||||
|
# fix it to the left-side index
|
||||||
|
index += len(self) + 1
|
||||||
|
if index >= len(self):
|
||||||
|
raise IndexError("Record index out of range")
|
||||||
|
return index
|
||||||
|
|
||||||
|
# interface methods
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.flush()
|
||||||
|
self.stream.close()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""Flush data to the associated stream."""
|
||||||
|
if self.changed:
|
||||||
|
self.header.setCurrentDate()
|
||||||
|
self.header.write(self.stream)
|
||||||
|
self.stream.flush()
|
||||||
|
self._changed = False
|
||||||
|
|
||||||
|
def indexOfFieldName(self, name):
|
||||||
|
"""Index of field named ``name``."""
|
||||||
|
# FIXME: move this to header class
|
||||||
|
names = [f.name for f in self.header.fields]
|
||||||
|
return names.index(name.upper())
|
||||||
|
|
||||||
|
def newRecord(self):
|
||||||
|
"""Return new record, which belong to this table."""
|
||||||
|
return self.RecordClass(self)
|
||||||
|
|
||||||
|
def append(self, record):
|
||||||
|
"""Append ``record`` to the database."""
|
||||||
|
record.index = self.header.recordCount
|
||||||
|
record._write()
|
||||||
|
self.header.recordCount += 1
|
||||||
|
self._changed = True
|
||||||
|
self._new = False
|
||||||
|
|
||||||
|
def addField(self, *defs):
|
||||||
|
"""Add field definitions.
|
||||||
|
|
||||||
|
For more information see `header.DbfHeader.addField`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._new:
|
||||||
|
self.header.addField(*defs)
|
||||||
|
else:
|
||||||
|
raise TypeError("At least one record was added, "
|
||||||
|
"structure can't be changed")
|
||||||
|
|
||||||
|
# 'magic' methods (representation and sequence interface)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Dbf stream '%s'\n" % self.stream + repr(self.header)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Return number of records."""
|
||||||
|
return self.recordCount
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
"""Return `DbfRecord` instance."""
|
||||||
|
return self.RecordClass.fromStream(self, self._fixIndex(index))
|
||||||
|
|
||||||
|
def __setitem__(self, index, record):
|
||||||
|
"""Write `DbfRecord` instance to the stream."""
|
||||||
|
record.index = self._fixIndex(index)
|
||||||
|
record._write()
|
||||||
|
self._changed = True
|
||||||
|
self._new = False
|
||||||
|
|
||||||
|
# def __del__(self):
|
||||||
|
# """Flush stream upon deletion of the object."""
|
||||||
|
# self.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_read(filename):
|
||||||
|
_dbf = Dbf(filename, True)
|
||||||
|
for _rec in _dbf:
|
||||||
|
print()
|
||||||
|
print(repr(_rec))
|
||||||
|
_dbf.close()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_create(filename):
|
||||||
|
_dbf = Dbf(filename, new=True)
|
||||||
|
_dbf.addField(
|
||||||
|
("NAME", "C", 15),
|
||||||
|
("SURNAME", "C", 25),
|
||||||
|
("INITIALS", "C", 10),
|
||||||
|
("BIRTHDATE", "D"),
|
||||||
|
)
|
||||||
|
for (_n, _s, _i, _b) in (
|
||||||
|
("John", "Miller", "YC", (1981, 1, 2)),
|
||||||
|
("Andy", "Larkin", "AL", (1982, 3, 4)),
|
||||||
|
("Bill", "Clinth", "", (1983, 5, 6)),
|
||||||
|
("Bobb", "McNail", "", (1984, 7, 8)),
|
||||||
|
):
|
||||||
|
_rec = _dbf.newRecord()
|
||||||
|
_rec["NAME"] = _n
|
||||||
|
_rec["SURNAME"] = _s
|
||||||
|
_rec["INITIALS"] = _i
|
||||||
|
_rec["BIRTHDATE"] = _b
|
||||||
|
_rec.store()
|
||||||
|
print(repr(_dbf))
|
||||||
|
_dbf.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_name = len(sys.argv) > 1 and sys.argv[1] or "county.dbf"
|
||||||
|
demo_create(_name)
|
||||||
|
demo_read(_name)
|
||||||
|
|
||||||
|
# vim: set et sw=4 sts=4 :
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
""".DBF creation helpers.
|
||||||
|
|
||||||
|
Note: this is a legacy interface. New code should use Dbf class
|
||||||
|
for table creation (see examples in dbf.py)
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- handle Memo fields.
|
||||||
|
- check length of the fields according to the
|
||||||
|
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""History (most recent first)
|
||||||
|
04-jul-2006 [als] added export declaration;
|
||||||
|
updated for dbfpy 2.0
|
||||||
|
15-dec-2005 [yc] define dbf_new.__slots__
|
||||||
|
14-dec-2005 [yc] added vim modeline; retab'd; added doc-strings;
|
||||||
|
dbf_new now is a new class (inherited from object)
|
||||||
|
??-jun-2000 [--] added by Hans Fiby
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||||
|
__date__ = "$Date: 2006/07/04 08:18:18 $"[7:-2]
|
||||||
|
|
||||||
|
__all__ = ["dbf_new"]
|
||||||
|
|
||||||
|
from .dbf import *
|
||||||
|
from .fields import *
|
||||||
|
from .header import *
|
||||||
|
from .record import *
|
||||||
|
|
||||||
|
|
||||||
|
class _FieldDefinition:
|
||||||
|
"""Field definition.
|
||||||
|
|
||||||
|
This is a simple structure, which contains ``name``, ``type``,
|
||||||
|
``len``, ``dec`` and ``cls`` fields.
|
||||||
|
|
||||||
|
Objects also implement get/setitem magic functions, so fields
|
||||||
|
could be accessed via sequence interface, where 'name' has
|
||||||
|
index 0, 'type' index 1, 'len' index 2, 'dec' index 3 and
|
||||||
|
'cls' could be located at index 4.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = "name", "type", "len", "dec", "cls"
|
||||||
|
|
||||||
|
# WARNING: be attentive - dictionaries are mutable!
|
||||||
|
FLD_TYPES = {
|
||||||
|
# type: (cls, len)
|
||||||
|
"C": (DbfCharacterFieldDef, None),
|
||||||
|
"N": (DbfNumericFieldDef, None),
|
||||||
|
"L": (DbfLogicalFieldDef, 1),
|
||||||
|
# FIXME: support memos
|
||||||
|
# "M": (DbfMemoFieldDef),
|
||||||
|
"D": (DbfDateFieldDef, 8),
|
||||||
|
# FIXME: I'm not sure length should be 14 characters!
|
||||||
|
# but temporary I use it, cuz date is 8 characters
|
||||||
|
# and time 6 (hhmmss)
|
||||||
|
"T": (DbfDateTimeFieldDef, 14),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, type, len=None, dec=0):
|
||||||
|
_cls, _len = self.FLD_TYPES[type]
|
||||||
|
if _len is None:
|
||||||
|
if len is None:
|
||||||
|
raise ValueError("Field length must be defined")
|
||||||
|
_len = len
|
||||||
|
self.name = name
|
||||||
|
self.type = type
|
||||||
|
self.len = _len
|
||||||
|
self.dec = dec
|
||||||
|
self.cls = _cls
|
||||||
|
|
||||||
|
def getDbfField(self):
|
||||||
|
"Return `DbfFieldDef` instance from the current definition."
|
||||||
|
return self.cls(self.name, self.len, self.dec)
|
||||||
|
|
||||||
|
def appendToHeader(self, dbfh):
|
||||||
|
"""Create a `DbfFieldDef` instance and append it to the dbf header.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dbfh: `DbfHeader` instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_dbff = self.getDbfField()
|
||||||
|
dbfh.addField(_dbff)
|
||||||
|
|
||||||
|
|
||||||
|
class dbf_new:
|
||||||
|
"""New .DBF creation helper.
|
||||||
|
|
||||||
|
Example Usage:
|
||||||
|
|
||||||
|
dbfn = dbf_new()
|
||||||
|
dbfn.add_field("name",'C',80)
|
||||||
|
dbfn.add_field("price",'N',10,2)
|
||||||
|
dbfn.add_field("date",'D',8)
|
||||||
|
dbfn.write("tst.dbf")
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This module cannot handle Memo-fields,
|
||||||
|
they are special.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("fields",)
|
||||||
|
|
||||||
|
FieldDefinitionClass = _FieldDefinition
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.fields = []
|
||||||
|
|
||||||
|
def add_field(self, name, typ, len, dec=0):
|
||||||
|
"""Add field definition.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name:
|
||||||
|
field name (str object). field name must not
|
||||||
|
contain ASCII NULs and it's length shouldn't
|
||||||
|
exceed 10 characters.
|
||||||
|
typ:
|
||||||
|
type of the field. this must be a single character
|
||||||
|
from the "CNLMDT" set meaning character, numeric,
|
||||||
|
logical, memo, date and date/time respectively.
|
||||||
|
len:
|
||||||
|
length of the field. this argument is used only for
|
||||||
|
the character and numeric fields. all other fields
|
||||||
|
have fixed length.
|
||||||
|
FIXME: use None as a default for this argument?
|
||||||
|
dec:
|
||||||
|
decimal precision. used only for the numric fields.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.fields.append(self.FieldDefinitionClass(name, typ, len, dec))
|
||||||
|
|
||||||
|
def write(self, filename):
|
||||||
|
"""Create empty .DBF file using current structure."""
|
||||||
|
_dbfh = DbfHeader()
|
||||||
|
_dbfh.setCurrentDate()
|
||||||
|
for _fldDef in self.fields:
|
||||||
|
_fldDef.appendToHeader(_dbfh)
|
||||||
|
|
||||||
|
_dbfStream = open(filename, "wb")
|
||||||
|
_dbfh.write(_dbfStream)
|
||||||
|
_dbfStream.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# create a new DBF-File
|
||||||
|
dbfn = dbf_new()
|
||||||
|
dbfn.add_field("name", 'C', 80)
|
||||||
|
dbfn.add_field("price", 'N', 10, 2)
|
||||||
|
dbfn.add_field("date", 'D', 8)
|
||||||
|
dbfn.write("tst.dbf")
|
||||||
|
# test new dbf
|
||||||
|
print("*** created tst.dbf: ***")
|
||||||
|
dbft = Dbf('tst.dbf', readOnly=0)
|
||||||
|
print(repr(dbft))
|
||||||
|
# add a record
|
||||||
|
rec = DbfRecord(dbft)
|
||||||
|
rec['name'] = 'something'
|
||||||
|
rec['price'] = 10.5
|
||||||
|
rec['date'] = (2000, 1, 12)
|
||||||
|
rec.store()
|
||||||
|
# add another record
|
||||||
|
rec = DbfRecord(dbft)
|
||||||
|
rec['name'] = 'foo and bar'
|
||||||
|
rec['price'] = 12234
|
||||||
|
rec['date'] = (1992, 7, 15)
|
||||||
|
rec.store()
|
||||||
|
|
||||||
|
# show the records
|
||||||
|
print("*** inserted 2 records into tst.dbf: ***")
|
||||||
|
print(repr(dbft))
|
||||||
|
for i1 in range(len(dbft)):
|
||||||
|
rec = dbft[i1]
|
||||||
|
for fldName in dbft.fieldNames:
|
||||||
|
print('{}:\t {}'.format(fldName, rec[fldName]))
|
||||||
|
print()
|
||||||
|
dbft.close()
|
||||||
|
|
||||||
|
# vim: set et sts=4 sw=4 :
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
"""DBF fields definitions.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- make memos work
|
||||||
|
"""
|
||||||
|
"""History (most recent first):
|
||||||
|
26-may-2009 [als] DbfNumericFieldDef.decodeValue: strip zero bytes
|
||||||
|
05-feb-2009 [als] DbfDateFieldDef.encodeValue: empty arg produces empty date
|
||||||
|
16-sep-2008 [als] DbfNumericFieldDef decoding looks for decimal point
|
||||||
|
in the value to select float or integer return type
|
||||||
|
13-mar-2008 [als] check field name length in constructor
|
||||||
|
11-feb-2007 [als] handle value conversion errors
|
||||||
|
10-feb-2007 [als] DbfFieldDef: added .rawFromRecord()
|
||||||
|
01-dec-2006 [als] Timestamp columns use None for empty values
|
||||||
|
31-oct-2006 [als] support field types 'F' (float), 'I' (integer)
|
||||||
|
and 'Y' (currency);
|
||||||
|
automate export and registration of field classes
|
||||||
|
04-jul-2006 [als] added export declaration
|
||||||
|
10-mar-2006 [als] decode empty values for Date and Logical fields;
|
||||||
|
show field name in errors
|
||||||
|
10-mar-2006 [als] fix Numeric value decoding: according to spec,
|
||||||
|
value always is string representation of the number;
|
||||||
|
ensure that encoded Numeric value fits into the field
|
||||||
|
20-dec-2005 [yc] use field names in upper case
|
||||||
|
15-dec-2005 [yc] field definitions moved from `dbf`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "$Revision: 1.14 $"[11:-2]
|
||||||
|
__date__ = "$Date: 2009/05/26 05:16:51 $"[7:-2]
|
||||||
|
|
||||||
|
__all__ = ["lookupFor"] # field classes added at the end of the module
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from functools import total_ordering
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
# abstract definitions
|
||||||
|
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class DbfFieldDef:
|
||||||
|
"""Abstract field definition.
|
||||||
|
|
||||||
|
Child classes must override ``type`` class attribute to provide datatype
|
||||||
|
information of the field definition. For more info about types visit
|
||||||
|
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||||
|
|
||||||
|
Also child classes must override ``defaultValue`` field to provide
|
||||||
|
default value for the field value.
|
||||||
|
|
||||||
|
If child class has fixed length ``length`` class attribute must be
|
||||||
|
overridden and set to the valid value. None value means, that field
|
||||||
|
isn't of fixed length.
|
||||||
|
|
||||||
|
Note: ``name`` field must not be changed after instantiation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("name", "decimalCount", "start", "end", "ignoreErrors")
|
||||||
|
|
||||||
|
# length of the field, None in case of variable-length field,
|
||||||
|
# or a number if this field is a fixed-length field
|
||||||
|
length = None
|
||||||
|
|
||||||
|
# field type. for more information about fields types visit
|
||||||
|
# `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||||
|
# must be overridden in child classes
|
||||||
|
typeCode = None
|
||||||
|
|
||||||
|
# default value for the field. this field must be
|
||||||
|
# overridden in child classes
|
||||||
|
defaultValue = None
|
||||||
|
|
||||||
|
def __init__(self, name, length=None, decimalCount=None,
|
||||||
|
start=None, stop=None, ignoreErrors=False):
|
||||||
|
"""Initialize instance."""
|
||||||
|
assert self.typeCode is not None, "Type code must be overridden"
|
||||||
|
assert self.defaultValue is not None, "Default value must be overridden"
|
||||||
|
# fix arguments
|
||||||
|
if len(name) > 10:
|
||||||
|
raise ValueError("Field name \"%s\" is too long" % name)
|
||||||
|
name = str(name).upper()
|
||||||
|
if self.__class__.length is None:
|
||||||
|
if length is None:
|
||||||
|
raise ValueError("[%s] Length isn't specified" % name)
|
||||||
|
length = int(length)
|
||||||
|
if length <= 0:
|
||||||
|
raise ValueError("[%s] Length must be a positive integer" % name)
|
||||||
|
else:
|
||||||
|
length = self.length
|
||||||
|
if decimalCount is None:
|
||||||
|
decimalCount = 0
|
||||||
|
# set fields
|
||||||
|
self.name = name
|
||||||
|
# FIXME: validate length according to the specification at
|
||||||
|
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html
|
||||||
|
self.length = length
|
||||||
|
self.decimalCount = decimalCount
|
||||||
|
self.ignoreErrors = ignoreErrors
|
||||||
|
self.start = start
|
||||||
|
self.end = stop
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return repr(self) == repr(other)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return repr(self) != repr(other)
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return repr(self) < repr(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.name)
|
||||||
|
|
||||||
|
def fromString(cls, string, start, ignoreErrors=False):
|
||||||
|
"""Decode dbf field definition from the string data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
string:
|
||||||
|
a string, dbf definition is decoded from. length of
|
||||||
|
the string must be 32 bytes.
|
||||||
|
start:
|
||||||
|
position in the database file.
|
||||||
|
ignoreErrors:
|
||||||
|
initial error processing mode for the new field (boolean)
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert len(string) == 32
|
||||||
|
_length = string[16]
|
||||||
|
return cls(utils.unzfill(string)[:11].decode('utf-8'), _length,
|
||||||
|
string[17], start, start + _length, ignoreErrors=ignoreErrors)
|
||||||
|
fromString = classmethod(fromString)
|
||||||
|
|
||||||
|
def toString(self):
|
||||||
|
"""Return encoded field definition.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is a string object containing encoded
|
||||||
|
definition of this field.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_name = self.name.ljust(11, '\0')
|
||||||
|
return (
|
||||||
|
_name +
|
||||||
|
self.typeCode +
|
||||||
|
# data address
|
||||||
|
chr(0) * 4 +
|
||||||
|
chr(self.length) +
|
||||||
|
chr(self.decimalCount) +
|
||||||
|
chr(0) * 14
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%-10s %1s %3d %3d" % self.fieldInfo()
|
||||||
|
|
||||||
|
def fieldInfo(self):
|
||||||
|
"""Return field information.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is a (name, type, length, decimals) tuple.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return (self.name, self.typeCode, self.length, self.decimalCount)
|
||||||
|
|
||||||
|
def rawFromRecord(self, record):
|
||||||
|
"""Return a "raw" field value from the record string."""
|
||||||
|
return record[self.start:self.end]
|
||||||
|
|
||||||
|
def decodeFromRecord(self, record):
|
||||||
|
"""Return decoded field value from the record string."""
|
||||||
|
try:
|
||||||
|
return self.decodeValue(self.rawFromRecord(record))
|
||||||
|
except Exception:
|
||||||
|
if self.ignoreErrors:
|
||||||
|
return utils.INVALID_VALUE
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return decoded value from string value.
|
||||||
|
|
||||||
|
This method shouldn't be used publicly. It's called from the
|
||||||
|
`decodeFromRecord` method.
|
||||||
|
|
||||||
|
This is an abstract method and it must be overridden in child classes.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return str object containing encoded field value.
|
||||||
|
|
||||||
|
This is an abstract method and it must be overridden in child classes.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# real classes
|
||||||
|
|
||||||
|
|
||||||
|
class DbfCharacterFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the character field."""
|
||||||
|
|
||||||
|
typeCode = "C"
|
||||||
|
defaultValue = b''
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return string object.
|
||||||
|
|
||||||
|
Return value is a ``value`` argument with stripped right spaces.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return value.rstrip(b' ').decode('utf-8')
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return raw data string encoded from a ``value``."""
|
||||||
|
return str(value)[:self.length].ljust(self.length)
|
||||||
|
|
||||||
|
|
||||||
|
class DbfNumericFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the numeric field."""
|
||||||
|
|
||||||
|
typeCode = "N"
|
||||||
|
# XXX: now I'm not sure it was a good idea to make a class field
|
||||||
|
# `defaultValue` instead of a generic method as it was implemented
|
||||||
|
# previously -- it's ok with all types except number, cuz
|
||||||
|
# if self.decimalCount is 0, we should return 0 and 0.0 otherwise.
|
||||||
|
defaultValue = 0
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return a number decoded from ``value``.
|
||||||
|
|
||||||
|
If decimals is zero, value will be decoded as an integer;
|
||||||
|
or as a float otherwise.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is a int (long) or float instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
value = value.strip(b' \0')
|
||||||
|
if b'.' in value:
|
||||||
|
# a float (has decimal separator)
|
||||||
|
return float(value)
|
||||||
|
elif value:
|
||||||
|
# must be an integer
|
||||||
|
return int(value)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return string containing encoded ``value``."""
|
||||||
|
_rv = ("%*.*f" % (self.length, self.decimalCount, value))
|
||||||
|
if len(_rv) > self.length:
|
||||||
|
_ppos = _rv.find(".")
|
||||||
|
if 0 <= _ppos <= self.length:
|
||||||
|
_rv = _rv[:self.length]
|
||||||
|
else:
|
||||||
|
raise ValueError("[%s] Numeric overflow: %s (field width: %i)"
|
||||||
|
% (self.name, _rv, self.length))
|
||||||
|
return _rv
|
||||||
|
|
||||||
|
|
||||||
|
class DbfFloatFieldDef(DbfNumericFieldDef):
|
||||||
|
"""Definition of the float field - same as numeric."""
|
||||||
|
|
||||||
|
typeCode = "F"
|
||||||
|
|
||||||
|
|
||||||
|
class DbfIntegerFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the integer field."""
|
||||||
|
|
||||||
|
typeCode = "I"
|
||||||
|
length = 4
|
||||||
|
defaultValue = 0
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return an integer number decoded from ``value``."""
|
||||||
|
return struct.unpack("<i", value)[0]
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return string containing encoded ``value``."""
|
||||||
|
return struct.pack("<i", int(value))
|
||||||
|
|
||||||
|
|
||||||
|
class DbfCurrencyFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the currency field."""
|
||||||
|
|
||||||
|
typeCode = "Y"
|
||||||
|
length = 8
|
||||||
|
defaultValue = 0.0
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return float number decoded from ``value``."""
|
||||||
|
return struct.unpack("<q", value)[0] / 10000.
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return string containing encoded ``value``."""
|
||||||
|
return struct.pack("<q", round(value * 10000))
|
||||||
|
|
||||||
|
|
||||||
|
class DbfLogicalFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the logical field."""
|
||||||
|
|
||||||
|
typeCode = "L"
|
||||||
|
defaultValue = -1
|
||||||
|
length = 1
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return True, False or -1 decoded from ``value``."""
|
||||||
|
# Note: value always is 1-char string
|
||||||
|
if value == "?":
|
||||||
|
return -1
|
||||||
|
if value in "NnFf ":
|
||||||
|
return False
|
||||||
|
if value in "YyTt":
|
||||||
|
return True
|
||||||
|
raise ValueError(f"[{self.name}] Invalid logical value {value!r}")
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return a character from the "TF?" set.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is "T" if ``value`` is True
|
||||||
|
"?" if value is -1 or False otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if value is True:
|
||||||
|
return "T"
|
||||||
|
if value == -1:
|
||||||
|
return "?"
|
||||||
|
return "F"
|
||||||
|
|
||||||
|
|
||||||
|
class DbfMemoFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the memo field.
|
||||||
|
|
||||||
|
Note: memos aren't currently completely supported.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
typeCode = "M"
|
||||||
|
defaultValue = " " * 10
|
||||||
|
length = 10
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return int .dbt block number decoded from the string object."""
|
||||||
|
# return int(value)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return raw data string encoded from a ``value``.
|
||||||
|
|
||||||
|
Note: this is an internal method.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# return str(value)[:self.length].ljust(self.length)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class DbfDateFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the date field."""
|
||||||
|
|
||||||
|
typeCode = "D"
|
||||||
|
defaultValue = utils.classproperty(lambda cls: datetime.date.today())
|
||||||
|
# "yyyymmdd" gives us 8 characters
|
||||||
|
length = 8
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return a ``datetime.date`` instance decoded from ``value``."""
|
||||||
|
if value.strip():
|
||||||
|
return utils.getDate(value)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return a string-encoded value.
|
||||||
|
|
||||||
|
``value`` argument should be a value suitable for the
|
||||||
|
`utils.getDate` call.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is a string in format "yyyymmdd".
|
||||||
|
|
||||||
|
"""
|
||||||
|
if value:
|
||||||
|
return utils.getDate(value).strftime("%Y%m%d")
|
||||||
|
else:
|
||||||
|
return " " * self.length
|
||||||
|
|
||||||
|
|
||||||
|
class DbfDateTimeFieldDef(DbfFieldDef):
|
||||||
|
"""Definition of the timestamp field."""
|
||||||
|
|
||||||
|
# a difference between JDN (Julian Day Number)
|
||||||
|
# and GDN (Gregorian Day Number). note, that GDN < JDN
|
||||||
|
JDN_GDN_DIFF = 1721425
|
||||||
|
typeCode = "T"
|
||||||
|
defaultValue = utils.classproperty(lambda cls: datetime.datetime.now())
|
||||||
|
# two 32-bits integers representing JDN and amount of
|
||||||
|
# milliseconds respectively gives us 8 bytes.
|
||||||
|
# note, that values must be encoded in LE byteorder.
|
||||||
|
length = 8
|
||||||
|
|
||||||
|
def decodeValue(self, value):
|
||||||
|
"""Return a `datetime.datetime` instance."""
|
||||||
|
assert len(value) == self.length
|
||||||
|
# LE byteorder
|
||||||
|
_jdn, _msecs = struct.unpack("<2I", value)
|
||||||
|
if _jdn >= 1:
|
||||||
|
_rv = datetime.datetime.fromordinal(_jdn - self.JDN_GDN_DIFF)
|
||||||
|
_rv += datetime.timedelta(0, _msecs / 1000.0)
|
||||||
|
else:
|
||||||
|
# empty date
|
||||||
|
_rv = None
|
||||||
|
return _rv
|
||||||
|
|
||||||
|
def encodeValue(self, value):
|
||||||
|
"""Return a string-encoded ``value``."""
|
||||||
|
if value:
|
||||||
|
value = utils.getDateTime(value)
|
||||||
|
# LE byteorder
|
||||||
|
_rv = struct.pack("<2I", value.toordinal() + self.JDN_GDN_DIFF,
|
||||||
|
(value.hour * 3600 + value.minute * 60 + value.second) * 1000)
|
||||||
|
else:
|
||||||
|
_rv = "\0" * self.length
|
||||||
|
assert len(_rv) == self.length
|
||||||
|
return _rv
|
||||||
|
|
||||||
|
|
||||||
|
_fieldsRegistry = {}
|
||||||
|
|
||||||
|
|
||||||
|
def registerField(fieldCls):
|
||||||
|
"""Register field definition class.
|
||||||
|
|
||||||
|
``fieldCls`` should be subclass of the `DbfFieldDef`.
|
||||||
|
|
||||||
|
Use `lookupFor` to retrieve field definition class
|
||||||
|
by the type code.
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert fieldCls.typeCode is not None, "Type code isn't defined"
|
||||||
|
# XXX: use fieldCls.typeCode.upper()? in case of any decign
|
||||||
|
# don't forget to look to the same comment in ``lookupFor`` method
|
||||||
|
_fieldsRegistry[fieldCls.typeCode] = fieldCls
|
||||||
|
|
||||||
|
|
||||||
|
def lookupFor(typeCode):
|
||||||
|
"""Return field definition class for the given type code.
|
||||||
|
|
||||||
|
``typeCode`` must be a single character. That type should be
|
||||||
|
previously registered.
|
||||||
|
|
||||||
|
Use `registerField` to register new field class.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Return value is a subclass of the `DbfFieldDef`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# XXX: use typeCode.upper()? in case of any decign don't
|
||||||
|
# forget to look to the same comment in ``registerField``
|
||||||
|
return _fieldsRegistry[chr(typeCode)]
|
||||||
|
|
||||||
|
# register generic types
|
||||||
|
|
||||||
|
|
||||||
|
for (_name, _val) in list(globals().items()):
|
||||||
|
if isinstance(_val, type) and issubclass(_val, DbfFieldDef) \
|
||||||
|
and (_name != "DbfFieldDef"):
|
||||||
|
__all__.append(_name)
|
||||||
|
registerField(_val)
|
||||||
|
del _name, _val
|
||||||
|
|
||||||
|
# vim: et sts=4 sw=4 :
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"""DBF header definition.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- handle encoding of the character fields
|
||||||
|
(encoding information stored in the DBF header)
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""History (most recent first):
|
||||||
|
16-sep-2010 [als] fromStream: fix century of the last update field
|
||||||
|
11-feb-2007 [als] added .ignoreErrors
|
||||||
|
10-feb-2007 [als] added __getitem__: return field definitions
|
||||||
|
by field name or field number (zero-based)
|
||||||
|
04-jul-2006 [als] added export declaration
|
||||||
|
15-dec-2005 [yc] created
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "$Revision: 1.6 $"[11:-2]
|
||||||
|
__date__ = "$Date: 2010/09/16 05:06:39 $"[7:-2]
|
||||||
|
|
||||||
|
__all__ = ["DbfHeader"]
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import fields
|
||||||
|
from .utils import getDate
|
||||||
|
|
||||||
|
|
||||||
|
class DbfHeader:
|
||||||
|
"""Dbf header definition.
|
||||||
|
|
||||||
|
For more information about dbf header format visit
|
||||||
|
`http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Create an empty dbf header and add some field definitions:
|
||||||
|
dbfh = DbfHeader()
|
||||||
|
dbfh.addField(("name", "C", 10))
|
||||||
|
dbfh.addField(("date", "D"))
|
||||||
|
dbfh.addField(DbfNumericFieldDef("price", 5, 2))
|
||||||
|
Create a dbf header with field definitions:
|
||||||
|
dbfh = DbfHeader([
|
||||||
|
("name", "C", 10),
|
||||||
|
("date", "D"),
|
||||||
|
DbfNumericFieldDef("price", 5, 2),
|
||||||
|
])
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("signature", "fields", "lastUpdate", "recordLength",
|
||||||
|
"recordCount", "headerLength", "changed", "_ignore_errors")
|
||||||
|
|
||||||
|
# instance construction and initialization methods
|
||||||
|
|
||||||
|
def __init__(self, fields=None, headerLength=0, recordLength=0,
|
||||||
|
recordCount=0, signature=0x03, lastUpdate=None, ignoreErrors=False):
|
||||||
|
"""Initialize instance.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
fields:
|
||||||
|
a list of field definitions;
|
||||||
|
recordLength:
|
||||||
|
size of the records;
|
||||||
|
headerLength:
|
||||||
|
size of the header;
|
||||||
|
recordCount:
|
||||||
|
number of records stored in DBF;
|
||||||
|
signature:
|
||||||
|
version number (aka signature). using 0x03 as a default meaning
|
||||||
|
"File without DBT". for more information about this field visit
|
||||||
|
``http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_NOTE_1_TARGET``
|
||||||
|
lastUpdate:
|
||||||
|
date of the DBF's update. this could be a string ('yymmdd' or
|
||||||
|
'yyyymmdd'), timestamp (int or float), datetime/date value,
|
||||||
|
a sequence (assuming (yyyy, mm, dd, ...)) or an object having
|
||||||
|
callable ``ticks`` field.
|
||||||
|
ignoreErrors:
|
||||||
|
error processing mode for DBF fields (boolean)
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.signature = signature
|
||||||
|
if fields is None:
|
||||||
|
self.fields = []
|
||||||
|
else:
|
||||||
|
self.fields = list(fields)
|
||||||
|
self.lastUpdate = getDate(lastUpdate)
|
||||||
|
self.recordLength = recordLength
|
||||||
|
self.headerLength = headerLength
|
||||||
|
self.recordCount = recordCount
|
||||||
|
self.ignoreErrors = ignoreErrors
|
||||||
|
# XXX: I'm not sure this is safe to
|
||||||
|
# initialize `self.changed` in this way
|
||||||
|
self.changed = bool(self.fields)
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
def fromString(cls, string):
|
||||||
|
"""Return header instance from the string object."""
|
||||||
|
return cls.fromStream(io.StringIO(str(string)))
|
||||||
|
fromString = classmethod(fromString)
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
def fromStream(cls, stream):
|
||||||
|
"""Return header object from the stream."""
|
||||||
|
stream.seek(0)
|
||||||
|
first_32 = stream.read(32)
|
||||||
|
if type(first_32) != bytes:
|
||||||
|
_data = bytes(first_32, sys.getfilesystemencoding())
|
||||||
|
_data = first_32
|
||||||
|
(_cnt, _hdrLen, _recLen) = struct.unpack("<I2H", _data[4:12])
|
||||||
|
# reserved = _data[12:32]
|
||||||
|
_year = _data[1]
|
||||||
|
if _year < 80:
|
||||||
|
# dBase II started at 1980. It is quite unlikely
|
||||||
|
# that actual last update date is before that year.
|
||||||
|
_year += 2000
|
||||||
|
else:
|
||||||
|
_year += 1900
|
||||||
|
# create header object
|
||||||
|
_obj = cls(None, _hdrLen, _recLen, _cnt, _data[0],
|
||||||
|
(_year, _data[2], _data[3]))
|
||||||
|
# append field definitions
|
||||||
|
# position 0 is for the deletion flag
|
||||||
|
_pos = 1
|
||||||
|
_data = stream.read(1)
|
||||||
|
while _data != b'\r':
|
||||||
|
_data += stream.read(31)
|
||||||
|
_fld = fields.lookupFor(_data[11]).fromString(_data, _pos)
|
||||||
|
_obj._addField(_fld)
|
||||||
|
_pos = _fld.end
|
||||||
|
_data = stream.read(1)
|
||||||
|
return _obj
|
||||||
|
fromStream = classmethod(fromStream)
|
||||||
|
|
||||||
|
# properties
|
||||||
|
|
||||||
|
year = property(lambda self: self.lastUpdate.year)
|
||||||
|
month = property(lambda self: self.lastUpdate.month)
|
||||||
|
day = property(lambda self: self.lastUpdate.day)
|
||||||
|
|
||||||
|
def ignoreErrors(self, value):
|
||||||
|
"""Update `ignoreErrors` flag on self and all fields"""
|
||||||
|
self._ignore_errors = value = bool(value)
|
||||||
|
for _field in self.fields:
|
||||||
|
_field.ignoreErrors = value
|
||||||
|
ignoreErrors = property(
|
||||||
|
lambda self: self._ignore_errors,
|
||||||
|
ignoreErrors,
|
||||||
|
doc="""Error processing mode for DBF field value conversion
|
||||||
|
|
||||||
|
if set, failing field value conversion will return
|
||||||
|
``INVALID_VALUE`` instead of raising conversion error.
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
# object representation
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
_rv = """\
|
||||||
|
Version (signature): 0x%02x
|
||||||
|
Last update: %s
|
||||||
|
Header length: %d
|
||||||
|
Record length: %d
|
||||||
|
Record count: %d
|
||||||
|
FieldName Type Len Dec
|
||||||
|
""" % (self.signature, self.lastUpdate, self.headerLength,
|
||||||
|
self.recordLength, self.recordCount)
|
||||||
|
_rv += "\n".join(
|
||||||
|
["%10s %4s %3s %3s" % _fld.fieldInfo() for _fld in self.fields]
|
||||||
|
)
|
||||||
|
return _rv
|
||||||
|
|
||||||
|
# internal methods
|
||||||
|
|
||||||
|
def _addField(self, *defs):
|
||||||
|
"""Internal variant of the `addField` method.
|
||||||
|
|
||||||
|
This method doesn't set `self.changed` field to True.
|
||||||
|
|
||||||
|
Return value is a length of the appended records.
|
||||||
|
Note: this method doesn't modify ``recordLength`` and
|
||||||
|
``headerLength`` fields. Use `addField` instead of this
|
||||||
|
method if you don't exactly know what you're doing.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# insure we have dbf.DbfFieldDef instances first (instantiation
|
||||||
|
# from the tuple could raise an error, in such a case I don't
|
||||||
|
# wanna add any of the definitions -- all will be ignored)
|
||||||
|
_defs = []
|
||||||
|
_recordLength = 0
|
||||||
|
for _def in defs:
|
||||||
|
if isinstance(_def, fields.DbfFieldDef):
|
||||||
|
_obj = _def
|
||||||
|
else:
|
||||||
|
(_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4]
|
||||||
|
_cls = fields.lookupFor(_type)
|
||||||
|
_obj = _cls(_name, _len, _dec, ignoreErrors=self._ignore_errors)
|
||||||
|
_recordLength += _obj.length
|
||||||
|
_defs.append(_obj)
|
||||||
|
# and now extend field definitions and
|
||||||
|
# update record length
|
||||||
|
self.fields += _defs
|
||||||
|
return _recordLength
|
||||||
|
|
||||||
|
# interface methods
|
||||||
|
|
||||||
|
def addField(self, *defs):
|
||||||
|
"""Add field definition to the header.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dbfh.addField(
|
||||||
|
("name", "C", 20),
|
||||||
|
dbf.DbfCharacterFieldDef("surname", 20),
|
||||||
|
dbf.DbfDateFieldDef("birthdate"),
|
||||||
|
("member", "L"),
|
||||||
|
)
|
||||||
|
dbfh.addField(("price", "N", 5, 2))
|
||||||
|
dbfh.addField(dbf.DbfNumericFieldDef("origprice", 5, 2))
|
||||||
|
|
||||||
|
"""
|
||||||
|
_oldLen = self.recordLength
|
||||||
|
self.recordLength += self._addField(*defs)
|
||||||
|
if not _oldLen:
|
||||||
|
self.recordLength += 1
|
||||||
|
# XXX: may be just use:
|
||||||
|
# self.recordeLength += self._addField(*defs) + bool(not _oldLen)
|
||||||
|
# recalculate headerLength
|
||||||
|
self.headerLength = 32 + (32 * len(self.fields)) + 1
|
||||||
|
self.changed = True
|
||||||
|
|
||||||
|
def write(self, stream):
|
||||||
|
"""Encode and write header to the stream."""
|
||||||
|
stream.seek(0)
|
||||||
|
stream.write(self.toString())
|
||||||
|
fields = [_fld.toString() for _fld in self.fields]
|
||||||
|
stream.write(''.join(fields).encode(sys.getfilesystemencoding()))
|
||||||
|
stream.write(b'\x0D') # cr at end of all header data
|
||||||
|
self.changed = False
|
||||||
|
|
||||||
|
def toString(self):
|
||||||
|
"""Returned 32 chars length string with encoded header."""
|
||||||
|
return struct.pack("<4BI2H",
|
||||||
|
self.signature,
|
||||||
|
self.year - 1900,
|
||||||
|
self.month,
|
||||||
|
self.day,
|
||||||
|
self.recordCount,
|
||||||
|
self.headerLength,
|
||||||
|
self.recordLength) + (b'\x00' * 20)
|
||||||
|
# TODO: figure out if bytes(utf-8) is correct here.
|
||||||
|
|
||||||
|
def setCurrentDate(self):
|
||||||
|
"""Update ``self.lastUpdate`` field with current date value."""
|
||||||
|
self.lastUpdate = datetime.date.today()
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
"""Return a field definition by numeric index or name string"""
|
||||||
|
if isinstance(item, str):
|
||||||
|
_name = item.upper()
|
||||||
|
for _field in self.fields:
|
||||||
|
if _field.name == _name:
|
||||||
|
return _field
|
||||||
|
else:
|
||||||
|
raise KeyError(item)
|
||||||
|
else:
|
||||||
|
# item must be field index
|
||||||
|
return self.fields[item]
|
||||||
|
|
||||||
|
# vim: et sts=4 sw=4 :
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
"""DBF record definition.
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""History (most recent first):
|
||||||
|
11-feb-2007 [als] __repr__: added special case for invalid field values
|
||||||
|
10-feb-2007 [als] added .rawFromStream()
|
||||||
|
30-oct-2006 [als] fix record length in .fromStream()
|
||||||
|
04-jul-2006 [als] added export declaration
|
||||||
|
20-dec-2005 [yc] DbfRecord.write() -> DbfRecord._write();
|
||||||
|
added delete() method.
|
||||||
|
16-dec-2005 [yc] record definition moved from `dbf`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||||
|
__date__ = "$Date: 2007/02/11 09:05:49 $"[7:-2]
|
||||||
|
|
||||||
|
__all__ = ["DbfRecord"]
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
class DbfRecord:
|
||||||
|
"""DBF record.
|
||||||
|
|
||||||
|
Instances of this class shouldn't be created manually,
|
||||||
|
use `dbf.Dbf.newRecord` instead.
|
||||||
|
|
||||||
|
Class implements mapping/sequence interface, so
|
||||||
|
fields could be accessed via their names or indexes
|
||||||
|
(names is a preferred way to access fields).
|
||||||
|
|
||||||
|
Hint:
|
||||||
|
Use `store` method to save modified record.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Add new record to the database:
|
||||||
|
db = Dbf(filename)
|
||||||
|
rec = db.newRecord()
|
||||||
|
rec["FIELD1"] = value1
|
||||||
|
rec["FIELD2"] = value2
|
||||||
|
rec.store()
|
||||||
|
Or the same, but modify existed
|
||||||
|
(second in this case) record:
|
||||||
|
db = Dbf(filename)
|
||||||
|
rec = db[2]
|
||||||
|
rec["FIELD1"] = value1
|
||||||
|
rec["FIELD2"] = value2
|
||||||
|
rec.store()
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = "dbf", "index", "deleted", "fieldData"
|
||||||
|
|
||||||
|
# creation and initialization
|
||||||
|
|
||||||
|
def __init__(self, dbf, index=None, deleted=False, data=None):
|
||||||
|
"""Instance initialization.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dbf:
|
||||||
|
A `Dbf.Dbf` instance this record belongs to.
|
||||||
|
index:
|
||||||
|
An integer record index or None. If this value is
|
||||||
|
None, record will be appended to the DBF.
|
||||||
|
deleted:
|
||||||
|
Boolean flag indicating whether this record
|
||||||
|
is a deleted record.
|
||||||
|
data:
|
||||||
|
A sequence or None. This is a data of the fields.
|
||||||
|
If this argument is None, default values will be used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.dbf = dbf
|
||||||
|
# XXX: I'm not sure ``index`` is necessary
|
||||||
|
self.index = index
|
||||||
|
self.deleted = deleted
|
||||||
|
if data is None:
|
||||||
|
self.fieldData = [_fd.defaultValue for _fd in dbf.header.fields]
|
||||||
|
else:
|
||||||
|
self.fieldData = list(data)
|
||||||
|
|
||||||
|
# XXX: validate self.index before calculating position?
|
||||||
|
position = property(lambda self: self.dbf.header.headerLength + \
|
||||||
|
self.index * self.dbf.header.recordLength)
|
||||||
|
|
||||||
|
def rawFromStream(cls, dbf, index):
|
||||||
|
"""Return raw record contents read from the stream.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dbf:
|
||||||
|
A `Dbf.Dbf` instance containing the record.
|
||||||
|
index:
|
||||||
|
Index of the record in the records' container.
|
||||||
|
This argument can't be None in this call.
|
||||||
|
|
||||||
|
Return value is a string containing record data in DBF format.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# XXX: may be write smth assuming, that current stream
|
||||||
|
# position is the required one? it could save some
|
||||||
|
# time required to calculate where to seek in the file
|
||||||
|
dbf.stream.seek(dbf.header.headerLength +
|
||||||
|
index * dbf.header.recordLength)
|
||||||
|
return dbf.stream.read(dbf.header.recordLength)
|
||||||
|
rawFromStream = classmethod(rawFromStream)
|
||||||
|
|
||||||
|
def fromStream(cls, dbf, index):
|
||||||
|
"""Return a record read from the stream.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dbf:
|
||||||
|
A `Dbf.Dbf` instance new record should belong to.
|
||||||
|
index:
|
||||||
|
Index of the record in the records' container.
|
||||||
|
This argument can't be None in this call.
|
||||||
|
|
||||||
|
Return value is an instance of the current class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return cls.fromString(dbf, cls.rawFromStream(dbf, index), index)
|
||||||
|
fromStream = classmethod(fromStream)
|
||||||
|
|
||||||
|
def fromString(cls, dbf, string, index=None):
|
||||||
|
"""Return record read from the string object.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dbf:
|
||||||
|
A `Dbf.Dbf` instance new record should belong to.
|
||||||
|
string:
|
||||||
|
A string new record should be created from.
|
||||||
|
index:
|
||||||
|
Index of the record in the container. If this
|
||||||
|
argument is None, record will be appended.
|
||||||
|
|
||||||
|
Return value is an instance of the current class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return cls(dbf, index, string[0]=="*",
|
||||||
|
[_fd.decodeFromRecord(string) for _fd in dbf.header.fields])
|
||||||
|
fromString = classmethod(fromString)
|
||||||
|
|
||||||
|
# object representation
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
_template = "%%%ds: %%s (%%s)" % max([len(_fld)
|
||||||
|
for _fld in self.dbf.fieldNames])
|
||||||
|
_rv = []
|
||||||
|
for _fld in self.dbf.fieldNames:
|
||||||
|
_val = self[_fld]
|
||||||
|
if _val is utils.INVALID_VALUE:
|
||||||
|
_rv.append(_template %
|
||||||
|
(_fld, "None", "value cannot be decoded"))
|
||||||
|
else:
|
||||||
|
_rv.append(_template % (_fld, _val, type(_val)))
|
||||||
|
return "\n".join(_rv)
|
||||||
|
|
||||||
|
# protected methods
|
||||||
|
|
||||||
|
def _write(self):
|
||||||
|
"""Write data to the dbf stream.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This isn't a public method, it's better to
|
||||||
|
use 'store' instead publicly.
|
||||||
|
Be design ``_write`` method should be called
|
||||||
|
only from the `Dbf` instance.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._validateIndex(False)
|
||||||
|
self.dbf.stream.seek(self.position)
|
||||||
|
self.dbf.stream.write(bytes(self.toString(),
|
||||||
|
sys.getfilesystemencoding()))
|
||||||
|
# FIXME: may be move this write somewhere else?
|
||||||
|
# why we should check this condition for each record?
|
||||||
|
if self.index == len(self.dbf):
|
||||||
|
# this is the last record,
|
||||||
|
# we should write SUB (ASCII 26)
|
||||||
|
self.dbf.stream.write(b"\x1A")
|
||||||
|
|
||||||
|
# utility methods
|
||||||
|
|
||||||
|
def _validateIndex(self, allowUndefined=True, checkRange=False):
|
||||||
|
"""Valid ``self.index`` value.
|
||||||
|
|
||||||
|
If ``allowUndefined`` argument is True functions does nothing
|
||||||
|
in case of ``self.index`` pointing to None object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.index is None:
|
||||||
|
if not allowUndefined:
|
||||||
|
raise ValueError("Index is undefined")
|
||||||
|
elif self.index < 0:
|
||||||
|
raise ValueError("Index can't be negative (%s)" % self.index)
|
||||||
|
elif checkRange and self.index <= self.dbf.header.recordCount:
|
||||||
|
raise ValueError("There are only %d records in the DBF" %
|
||||||
|
self.dbf.header.recordCount)
|
||||||
|
|
||||||
|
# interface methods
|
||||||
|
|
||||||
|
def store(self):
|
||||||
|
"""Store current record in the DBF.
|
||||||
|
|
||||||
|
If ``self.index`` is None, this record will be appended to the
|
||||||
|
records of the DBF this records belongs to; or replaced otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._validateIndex()
|
||||||
|
if self.index is None:
|
||||||
|
self.index = len(self.dbf)
|
||||||
|
self.dbf.append(self)
|
||||||
|
else:
|
||||||
|
self.dbf[self.index] = self
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""Mark method as deleted."""
|
||||||
|
self.deleted = True
|
||||||
|
|
||||||
|
def toString(self):
|
||||||
|
"""Return string packed record values."""
|
||||||
|
# for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData):
|
||||||
|
#
|
||||||
|
|
||||||
|
return "".join([" *"[self.deleted]] + [
|
||||||
|
_def.encodeValue(_dat)
|
||||||
|
for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData)
|
||||||
|
])
|
||||||
|
|
||||||
|
def asList(self):
|
||||||
|
"""Return a flat list of fields.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Change of the list's values won't change
|
||||||
|
real values stored in this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.fieldData[:]
|
||||||
|
|
||||||
|
def asDict(self):
|
||||||
|
"""Return a dictionary of fields.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Change of the dicts's values won't change
|
||||||
|
real values stored in this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return dict([_i for _i in zip(self.dbf.fieldNames, self.fieldData)])
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""Return value by field name or field index."""
|
||||||
|
if isinstance(key, int):
|
||||||
|
# integer index of the field
|
||||||
|
return self.fieldData[key]
|
||||||
|
# assuming string field name
|
||||||
|
return self.fieldData[self.dbf.indexOfFieldName(key)]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""Set field value by integer index of the field or string name."""
|
||||||
|
if isinstance(key, int):
|
||||||
|
# integer index of the field
|
||||||
|
return self.fieldData[key]
|
||||||
|
# assuming string field name
|
||||||
|
self.fieldData[self.dbf.indexOfFieldName(key)] = value
|
||||||
|
|
||||||
|
# vim: et sts=4 sw=4 :
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""String utilities.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- allow strings in getDateTime routine;
|
||||||
|
"""
|
||||||
|
"""History (most recent first):
|
||||||
|
11-feb-2007 [als] added INVALID_VALUE
|
||||||
|
10-feb-2007 [als] allow date strings padded with spaces instead of zeroes
|
||||||
|
20-dec-2005 [yc] handle long objects in getDate/getDateTime
|
||||||
|
16-dec-2005 [yc] created from ``strutil`` module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||||
|
__date__ = "$Date: 2007/02/11 08:57:17 $"[7:-2]
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def unzfill(str):
|
||||||
|
"""Return a string without ASCII NULs.
|
||||||
|
|
||||||
|
This function searchers for the first NUL (ASCII 0) occurrence
|
||||||
|
and truncates string till that position.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return str[:str.index(b'\0')]
|
||||||
|
except ValueError:
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def getDate(date=None):
|
||||||
|
"""Return `datetime.date` instance.
|
||||||
|
|
||||||
|
Type of the ``date`` argument could be one of the following:
|
||||||
|
None:
|
||||||
|
use current date value;
|
||||||
|
datetime.date:
|
||||||
|
this value will be returned;
|
||||||
|
datetime.datetime:
|
||||||
|
the result of the date.date() will be returned;
|
||||||
|
string:
|
||||||
|
assuming "%Y%m%d" or "%y%m%dd" format;
|
||||||
|
number:
|
||||||
|
assuming it's a timestamp (returned for example
|
||||||
|
by the time.time() call;
|
||||||
|
sequence:
|
||||||
|
assuming (year, month, day, ...) sequence;
|
||||||
|
|
||||||
|
Additionally, if ``date`` has callable ``ticks`` attribute,
|
||||||
|
it will be used and result of the called would be treated
|
||||||
|
as a timestamp value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if date is None:
|
||||||
|
# use current value
|
||||||
|
return datetime.date.today()
|
||||||
|
if isinstance(date, datetime.date):
|
||||||
|
return date
|
||||||
|
if isinstance(date, datetime.datetime):
|
||||||
|
return date.date()
|
||||||
|
if isinstance(date, (int, float)):
|
||||||
|
# date is a timestamp
|
||||||
|
return datetime.date.fromtimestamp(date)
|
||||||
|
if isinstance(date, str):
|
||||||
|
date = date.replace(" ", "0")
|
||||||
|
if len(date) == 6:
|
||||||
|
# yymmdd
|
||||||
|
return datetime.date(*time.strptime(date, "%y%m%d")[:3])
|
||||||
|
# yyyymmdd
|
||||||
|
return datetime.date(*time.strptime(date, "%Y%m%d")[:3])
|
||||||
|
if hasattr(date, "__getitem__"):
|
||||||
|
# a sequence (assuming date/time tuple)
|
||||||
|
return datetime.date(*date[:3])
|
||||||
|
return datetime.date.fromtimestamp(date.ticks())
|
||||||
|
|
||||||
|
|
||||||
|
def getDateTime(value=None):
|
||||||
|
"""Return `datetime.datetime` instance.
|
||||||
|
|
||||||
|
Type of the ``value`` argument could be one of the following:
|
||||||
|
None:
|
||||||
|
use current date value;
|
||||||
|
datetime.date:
|
||||||
|
result will be converted to the `datetime.datetime` instance
|
||||||
|
using midnight;
|
||||||
|
datetime.datetime:
|
||||||
|
``value`` will be returned as is;
|
||||||
|
string:
|
||||||
|
*** CURRENTLY NOT SUPPORTED ***;
|
||||||
|
number:
|
||||||
|
assuming it's a timestamp (returned for example
|
||||||
|
by the time.time() call;
|
||||||
|
sequence:
|
||||||
|
assuming (year, month, day, ...) sequence;
|
||||||
|
|
||||||
|
Additionally, if ``value`` has callable ``ticks`` attribute,
|
||||||
|
it will be used and result of the called would be treated
|
||||||
|
as a timestamp value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
# use current value
|
||||||
|
return datetime.datetime.today()
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
return value
|
||||||
|
if isinstance(value, datetime.date):
|
||||||
|
return datetime.datetime.fromordinal(value.toordinal())
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
# value is a timestamp
|
||||||
|
return datetime.datetime.fromtimestamp(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
raise NotImplementedError("Strings aren't currently implemented")
|
||||||
|
if hasattr(value, "__getitem__"):
|
||||||
|
# a sequence (assuming date/time tuple)
|
||||||
|
return datetime.datetime(*tuple(value)[:6])
|
||||||
|
return datetime.datetime.fromtimestamp(value.ticks())
|
||||||
|
|
||||||
|
|
||||||
|
class classproperty(property):
|
||||||
|
"""Works in the same way as a ``property``, but for the classes."""
|
||||||
|
|
||||||
|
def __get__(self, obj, cls):
|
||||||
|
return self.fget(cls)
|
||||||
|
|
||||||
|
|
||||||
|
class _InvalidValue:
|
||||||
|
|
||||||
|
"""Value returned from DBF records when field validation fails
|
||||||
|
|
||||||
|
The value is not equal to anything except for itself
|
||||||
|
and equal to all empty values: None, 0, empty string etc.
|
||||||
|
In other words, invalid value is equal to None and not equal
|
||||||
|
to None at the same time.
|
||||||
|
|
||||||
|
This value yields zero upon explicit conversion to a number type,
|
||||||
|
empty string for string types, and False for boolean.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return not other
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (other is self)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return 0
|
||||||
|
__long__ = __int__
|
||||||
|
|
||||||
|
def __float__(self):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<INVALID>"
|
||||||
|
|
||||||
|
|
||||||
|
# invalid value is a constant singleton
|
||||||
|
INVALID_VALUE = _InvalidValue()
|
||||||
|
|
||||||
|
# vim: set et sts=4 sw=4 :
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from io import BytesIO, StringIO
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_input(stream):
|
||||||
|
"""
|
||||||
|
Accept either a str/bytes stream or a file-like object and always return a
|
||||||
|
file-like object.
|
||||||
|
"""
|
||||||
|
if isinstance(stream, str):
|
||||||
|
return StringIO(stream)
|
||||||
|
elif isinstance(stream, bytes):
|
||||||
|
return BytesIO(stream)
|
||||||
|
return stream
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
""" Tablib.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tablib.core import (
|
|
||||||
Databook, Dataset, InvalidDatasetType,
|
|
||||||
InvalidDimensions, UnsupportedFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
-281
@@ -1,281 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - Core Library.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tablib.formats import FORMATS as formats
|
|
||||||
|
|
||||||
|
|
||||||
__title__ = 'tablib'
|
|
||||||
__version__ = '0.8.1'
|
|
||||||
__build__ = 0x000801
|
|
||||||
__author__ = 'Kenneth Reitz'
|
|
||||||
__license__ = 'MIT'
|
|
||||||
__copyright__ = 'Copyright 2010 Kenneth Reitz'
|
|
||||||
|
|
||||||
|
|
||||||
class Dataset(object):
|
|
||||||
"""Epic Tabular-Dataset object. """
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._data = list(args)
|
|
||||||
self.__headers = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.headers = kwargs['headers']
|
|
||||||
except KeyError:
|
|
||||||
self.headers = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.title = kwargs['title']
|
|
||||||
except KeyError:
|
|
||||||
self.title = None
|
|
||||||
|
|
||||||
self._register_formats()
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return self.height
|
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if isinstance(key, basestring):
|
|
||||||
if key in self.headers:
|
|
||||||
pos = self.headers.index(key) # get 'key' index from each data
|
|
||||||
return [row[pos] for row in self._data]
|
|
||||||
else:
|
|
||||||
raise KeyError
|
|
||||||
else:
|
|
||||||
return self._data[key]
|
|
||||||
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self._validate(value)
|
|
||||||
self._data[key] = tuple(value)
|
|
||||||
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
del self._data[key]
|
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
try:
|
|
||||||
return '<%s dataset>' % (self.title.lower())
|
|
||||||
except AttributeError:
|
|
||||||
return '<dataset object>'
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _register_formats(cls):
|
|
||||||
"""Adds format properties."""
|
|
||||||
for fmt in formats:
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
setattr(cls, fmt.title, property(fmt.export_set, fmt.import_set))
|
|
||||||
except AttributeError:
|
|
||||||
setattr(cls, fmt.title, property(fmt.export_set))
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _validate(self, row=None, col=None, safety=False):
|
|
||||||
"""Assures size of every row in dataset is of proper proportions."""
|
|
||||||
if row:
|
|
||||||
is_valid = (len(row) == self.width) if self.width else True
|
|
||||||
elif col:
|
|
||||||
if self.headers:
|
|
||||||
is_valid = (len(col) - 1) == self.height
|
|
||||||
else:
|
|
||||||
is_valid = (len(col) == self.height) if self.height else True
|
|
||||||
else:
|
|
||||||
is_valid = all((len(x)== self.width for x in self._data))
|
|
||||||
|
|
||||||
if is_valid:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
if not safety:
|
|
||||||
raise InvalidDimensions
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _package(self, dicts=True):
|
|
||||||
"""Packages Dataset into lists of dictionaries for transmission."""
|
|
||||||
|
|
||||||
if self.headers:
|
|
||||||
if dicts:
|
|
||||||
data = [dict(zip(self.headers, data_row)) for data_row in self ._data]
|
|
||||||
else:
|
|
||||||
data = [list(self.headers)] + list(self._data)
|
|
||||||
else:
|
|
||||||
data = [list(row) for row in self._data]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def height(self):
|
|
||||||
"""Returns the height of the Dataset."""
|
|
||||||
return len(self._data)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def width(self):
|
|
||||||
"""Returns the width of the Dataset."""
|
|
||||||
try:
|
|
||||||
return len(self._data[0])
|
|
||||||
except IndexError:
|
|
||||||
try:
|
|
||||||
return len(self.headers)
|
|
||||||
except TypeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def headers(self):
|
|
||||||
"""Headers property."""
|
|
||||||
return self.__headers
|
|
||||||
|
|
||||||
|
|
||||||
@headers.setter
|
|
||||||
def headers(self, collection):
|
|
||||||
"""Validating headers setter."""
|
|
||||||
self._validate(collection)
|
|
||||||
if collection:
|
|
||||||
try:
|
|
||||||
self.__headers = list(collection)
|
|
||||||
except TypeError:
|
|
||||||
raise TypeError
|
|
||||||
else:
|
|
||||||
self.__headers = None
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dict(self):
|
|
||||||
"""Returns python dict of Dataset."""
|
|
||||||
return self._package()
|
|
||||||
|
|
||||||
|
|
||||||
@dict.setter
|
|
||||||
def dict(self, pickle):
|
|
||||||
"""Returns python dict of Dataset."""
|
|
||||||
if not len(pickle):
|
|
||||||
return
|
|
||||||
if isinstance(pickle[0], list):
|
|
||||||
for row in pickle:
|
|
||||||
self.append(row)
|
|
||||||
elif isinstance(pickle[0], dict):
|
|
||||||
self.headers = pickle[0].keys()
|
|
||||||
for row in pickle:
|
|
||||||
self.append(row.values())
|
|
||||||
else:
|
|
||||||
raise UnsupportedFormat
|
|
||||||
|
|
||||||
|
|
||||||
def append(self, row=None, col=None):
|
|
||||||
"""Adds a row to the end of Dataset"""
|
|
||||||
if row:
|
|
||||||
self._validate(row)
|
|
||||||
self._data.append(tuple(row))
|
|
||||||
elif col:
|
|
||||||
self._validate(col=col)
|
|
||||||
|
|
||||||
if self.headers:
|
|
||||||
# pop the first item off, add to headers
|
|
||||||
self.headers.append(col[0])
|
|
||||||
col = col[1:]
|
|
||||||
|
|
||||||
if self.height and self.width:
|
|
||||||
|
|
||||||
for i, row in enumerate(self._data):
|
|
||||||
_row = list(row)
|
|
||||||
_row.append(col[i])
|
|
||||||
self._data[i] = tuple(_row)
|
|
||||||
else:
|
|
||||||
self._data = [tuple([row]) for row in col]
|
|
||||||
|
|
||||||
|
|
||||||
def insert(self, i, row=None, col=None):
|
|
||||||
"""Inserts a row at given position in Dataset"""
|
|
||||||
if row:
|
|
||||||
self._validate(row)
|
|
||||||
self._data.insert(i, tuple(row))
|
|
||||||
elif col:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def wipe(self):
|
|
||||||
"""Erases all data from Dataset."""
|
|
||||||
self._data = list()
|
|
||||||
self.__headers = None
|
|
||||||
|
|
||||||
|
|
||||||
class Databook(object):
|
|
||||||
"""A book of Dataset objects.
|
|
||||||
Currently, this exists only for XLS workbook support.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sets=[]):
|
|
||||||
self._datasets = sets
|
|
||||||
self._register_formats()
|
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
try:
|
|
||||||
return '<%s databook>' % (self.title.lower())
|
|
||||||
except AttributeError:
|
|
||||||
return '<databook object>'
|
|
||||||
|
|
||||||
def wipe(self):
|
|
||||||
"""Wipe book clean."""
|
|
||||||
self._datasets = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _register_formats(cls):
|
|
||||||
"""Adds format properties."""
|
|
||||||
for fmt in formats:
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
setattr(cls, fmt.title, property(fmt.export_book, fmt.import_book))
|
|
||||||
except AttributeError:
|
|
||||||
setattr(cls, fmt.title, property(fmt.export_book))
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def add_sheet(self, dataset):
|
|
||||||
"""Adds given dataset."""
|
|
||||||
if type(dataset) is Dataset:
|
|
||||||
self._datasets.append(dataset)
|
|
||||||
else:
|
|
||||||
raise InvalidDatasetType
|
|
||||||
|
|
||||||
|
|
||||||
def _package(self):
|
|
||||||
"""Packages Databook for delivery."""
|
|
||||||
collector = []
|
|
||||||
for dset in self._datasets:
|
|
||||||
collector.append(dict(
|
|
||||||
title = dset.title,
|
|
||||||
data = dset.dict
|
|
||||||
))
|
|
||||||
return collector
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self):
|
|
||||||
"""The number of the Datasets within DataBook."""
|
|
||||||
return len(self._datasets)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDatasetType(Exception):
|
|
||||||
"Only Datasets can be added to a DataBook"
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDimensions(Exception):
|
|
||||||
"Invalid size"
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFormat(NotImplementedError):
|
|
||||||
"Format is not supported"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - formats
|
|
||||||
"""
|
|
||||||
|
|
||||||
import _csv as csv
|
|
||||||
import _json as json
|
|
||||||
import _xls as xls
|
|
||||||
import _yaml as yaml
|
|
||||||
|
|
||||||
FORMATS = (csv, json, xls, yaml)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - CSV Support.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import cStringIO
|
|
||||||
import csv
|
|
||||||
import os
|
|
||||||
|
|
||||||
import simplejson as json
|
|
||||||
|
|
||||||
import tablib
|
|
||||||
|
|
||||||
|
|
||||||
title = 'csv'
|
|
||||||
extentions = ('csv',)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def export_set(dataset):
|
|
||||||
"""Returns CSV representation of Dataset."""
|
|
||||||
stream = cStringIO.StringIO()
|
|
||||||
_csv = csv.writer(stream)
|
|
||||||
|
|
||||||
for row in dataset._package(dicts=False):
|
|
||||||
_csv.writerow(row)
|
|
||||||
|
|
||||||
return stream.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
def import_set(dset, in_stream, headers=True):
|
|
||||||
"""Returns dataset from CSV stream."""
|
|
||||||
|
|
||||||
dset.wipe()
|
|
||||||
|
|
||||||
rows = csv.reader(in_stream.split())
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
|
|
||||||
if (i == 0) and (headers):
|
|
||||||
dset.headers = row
|
|
||||||
else:
|
|
||||||
dset.append(row)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - JSON Support
|
|
||||||
"""
|
|
||||||
|
|
||||||
import simplejson as json
|
|
||||||
import tablib.core
|
|
||||||
|
|
||||||
title = 'json'
|
|
||||||
extentions = ('json', 'jsn')
|
|
||||||
|
|
||||||
|
|
||||||
def export_set(dataset):
|
|
||||||
"""Returns JSON representation of Dataset."""
|
|
||||||
return json.dumps(dataset.dict)
|
|
||||||
|
|
||||||
|
|
||||||
def export_book(databook):
|
|
||||||
"""Returns JSON representation of Databook."""
|
|
||||||
return json.dumps(databook._package())
|
|
||||||
|
|
||||||
|
|
||||||
def import_set(dset, in_stream):
|
|
||||||
"""Returns dataset from JSON stream."""
|
|
||||||
|
|
||||||
dset.wipe()
|
|
||||||
dset.dict = json.loads(in_stream)
|
|
||||||
|
|
||||||
|
|
||||||
def import_book(dbook, in_stream):
|
|
||||||
"""Returns databook from JSON stream."""
|
|
||||||
|
|
||||||
dbook.wipe()
|
|
||||||
for sheet in json.loads(in_stream):
|
|
||||||
data = tablib.core.Dataset()
|
|
||||||
data.title = sheet['title']
|
|
||||||
data.dict = sheet['data']
|
|
||||||
dbook.add_sheet(data)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - XLS Support.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import xlwt
|
|
||||||
import cStringIO
|
|
||||||
|
|
||||||
|
|
||||||
title = 'xls'
|
|
||||||
extentions = ('xls',)
|
|
||||||
|
|
||||||
|
|
||||||
def export_set(dataset):
|
|
||||||
"""Returns XLS representation of Dataset."""
|
|
||||||
|
|
||||||
wb = xlwt.Workbook(encoding='utf8')
|
|
||||||
ws = wb.add_sheet(dataset.title if dataset.title else 'Tabbed Dataset')
|
|
||||||
|
|
||||||
for i, row in enumerate(dataset._package(dicts=False)):
|
|
||||||
for j, col in enumerate(row):
|
|
||||||
ws.write(i, j, col)
|
|
||||||
|
|
||||||
stream = cStringIO.StringIO()
|
|
||||||
wb.save(stream)
|
|
||||||
return stream.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
def export_book(databook):
|
|
||||||
"""Returns XLS representation of DataBook."""
|
|
||||||
|
|
||||||
wb = xlwt.Workbook(encoding='utf8')
|
|
||||||
|
|
||||||
for i, dset in enumerate(databook._datasets):
|
|
||||||
ws = wb.add_sheet(dset.title if dset.title else 'Sheet%s' % (i))
|
|
||||||
|
|
||||||
#for row in self._package(dicts=False):
|
|
||||||
for i, row in enumerate(dset._package(dicts=False)):
|
|
||||||
for j, col in enumerate(row):
|
|
||||||
ws.write(i, j, col)
|
|
||||||
|
|
||||||
|
|
||||||
stream = cStringIO.StringIO()
|
|
||||||
wb.save(stream)
|
|
||||||
return stream.getvalue()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - YAML Support.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
import tablib
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
title = 'yaml'
|
|
||||||
extentions = ('yaml', 'yml')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def export_set(dataset):
|
|
||||||
"""Returns YAML representation of Dataset."""
|
|
||||||
return yaml.dump(dataset.dict)
|
|
||||||
|
|
||||||
|
|
||||||
def export_book(databook):
|
|
||||||
"""Returns YAML representation of Databook."""
|
|
||||||
return yaml.dump(databook._package())
|
|
||||||
|
|
||||||
|
|
||||||
def import_set(dset, in_stream):
|
|
||||||
"""Returns dataset from YAML stream."""
|
|
||||||
|
|
||||||
dset.wipe()
|
|
||||||
dset.dict = yaml.load(in_stream)
|
|
||||||
|
|
||||||
|
|
||||||
def import_book(dbook, in_stream):
|
|
||||||
"""Returns databook from YAML stream."""
|
|
||||||
|
|
||||||
dbook.wipe()
|
|
||||||
|
|
||||||
for sheet in yaml.load(in_stream):
|
|
||||||
data = tablib.core.Dataset()
|
|
||||||
data.title = sheet['title']
|
|
||||||
data.dict = sheet['data']
|
|
||||||
dbook.add_sheet(data)
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Tablib - General Helpers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class Struct(object):
|
|
||||||
"""Your attributes are belong to us."""
|
|
||||||
|
|
||||||
def __init__(self, **entries):
|
|
||||||
self.__dict__.update(entries)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return getattr(self, key, None)
|
|
||||||
|
|
||||||
def dictionary(self):
|
|
||||||
"""Returns dictionary representation of object."""
|
|
||||||
return self.__dict__
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
"""Returns items within object."""
|
|
||||||
return self.__dict__.items()
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
"""Returns keys within object."""
|
|
||||||
return self.__dict__.keys()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def piped():
|
|
||||||
"""Returns piped input via stdin, else False."""
|
|
||||||
with sys.stdin as stdin:
|
|
||||||
# TTY is only way to detect if stdin contains data
|
|
||||||
return stdin.read() if not stdin.isatty() else None
|
|
||||||
|
|
||||||
-283
@@ -1,283 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""Tests for tablib."""
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import tablib
|
|
||||||
|
|
||||||
|
|
||||||
class TablibTestCase(unittest.TestCase):
|
|
||||||
"""Tablib test cases."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Create simple data set with headers."""
|
|
||||||
|
|
||||||
global data, book
|
|
||||||
data = tablib.Dataset()
|
|
||||||
book = tablib.Databook()
|
|
||||||
|
|
||||||
self.headers = ('first_name', 'last_name', 'gpa')
|
|
||||||
self.john = ('John', 'Adams', 90)
|
|
||||||
self.george = ('George', 'Washington', 67)
|
|
||||||
self.tom = ('Thomas', 'Jefferson', 50)
|
|
||||||
|
|
||||||
self.founders = tablib.Dataset(headers=self.headers)
|
|
||||||
self.founders.append(self.john)
|
|
||||||
self.founders.append(self.george)
|
|
||||||
self.founders.append(self.tom)
|
|
||||||
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Teardown."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_append(self):
|
|
||||||
"""Verify append() correctly adds tuple with no headers."""
|
|
||||||
new_row = (1, 2, 3)
|
|
||||||
data.append(new_row)
|
|
||||||
|
|
||||||
# Verify width/data
|
|
||||||
self.assertTrue(data.width == len(new_row))
|
|
||||||
self.assertTrue(data[0] == new_row)
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_append_with_headers(self):
|
|
||||||
"""Verify append() correctly detects mismatch of number of
|
|
||||||
headers and data.
|
|
||||||
"""
|
|
||||||
data.headers = ['first', 'second']
|
|
||||||
new_row = (1, 2, 3, 4)
|
|
||||||
|
|
||||||
self.assertRaises(tablib.InvalidDimensions, data.append, new_row)
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_column(self):
|
|
||||||
"""Verify adding column works with/without headers."""
|
|
||||||
|
|
||||||
data.append(['kenneth'])
|
|
||||||
data.append(['bessie'])
|
|
||||||
|
|
||||||
new_col = ['reitz', 'monke']
|
|
||||||
|
|
||||||
data.append(col=new_col)
|
|
||||||
|
|
||||||
self.assertEquals(data[0], ('kenneth', 'reitz'))
|
|
||||||
self.assertEquals(data.width, 2)
|
|
||||||
|
|
||||||
# With Headers
|
|
||||||
data.headers = ('fname', 'lname')
|
|
||||||
new_col = ['age', 21, 22]
|
|
||||||
data.append(col=new_col)
|
|
||||||
|
|
||||||
self.assertEquals(data[new_col[0]], new_col[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_column_no_data_no_headers(self):
|
|
||||||
"""Verify adding new column with no headers."""
|
|
||||||
|
|
||||||
new_col = ('reitz', 'monke')
|
|
||||||
|
|
||||||
data.append(col=new_col)
|
|
||||||
|
|
||||||
self.assertEquals(data[0], tuple([new_col[0]]))
|
|
||||||
self.assertEquals(data.width, 1)
|
|
||||||
self.assertEquals(data.height, len(new_col))
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_column_no_data_with_headers(self):
|
|
||||||
"""Verify adding new column with headers."""
|
|
||||||
|
|
||||||
data.headers = ('first', 'last')
|
|
||||||
|
|
||||||
new_col = ('age',)
|
|
||||||
data.append(col=new_col)
|
|
||||||
|
|
||||||
self.assertEquals(len(data.headers), 3)
|
|
||||||
self.assertEquals(data.width, 3)
|
|
||||||
|
|
||||||
new_col = ('foo', 'bar')
|
|
||||||
|
|
||||||
self.assertRaises(tablib.InvalidDimensions, data.append, col=new_col)
|
|
||||||
|
|
||||||
|
|
||||||
def test_header_slicing(self):
|
|
||||||
"""Verify slicing by headers."""
|
|
||||||
|
|
||||||
self.assertEqual(self.founders['first_name'],
|
|
||||||
[self.john[0], self.george[0], self.tom[0]])
|
|
||||||
self.assertEqual(self.founders['last_name'],
|
|
||||||
[self.john[1], self.george[1], self.tom[1]])
|
|
||||||
self.assertEqual(self.founders['gpa'],
|
|
||||||
[self.john[2], self.george[2], self.tom[2]])
|
|
||||||
|
|
||||||
|
|
||||||
def test_data_slicing(self):
|
|
||||||
"""Verify slicing by data."""
|
|
||||||
|
|
||||||
# Slice individual rows
|
|
||||||
self.assertEqual(self.founders[0], self.john)
|
|
||||||
self.assertEqual(self.founders[:1], [self.john])
|
|
||||||
self.assertEqual(self.founders[1:2], [self.george])
|
|
||||||
self.assertEqual(self.founders[-1], self.tom)
|
|
||||||
self.assertEqual(self.founders[3:], [])
|
|
||||||
|
|
||||||
# Slice multiple rows
|
|
||||||
self.assertEqual(self.founders[:], [self.john, self.george, self.tom])
|
|
||||||
self.assertEqual(self.founders[0:2], [self.john, self.george])
|
|
||||||
self.assertEqual(self.founders[1:3], [self.george, self.tom])
|
|
||||||
self.assertEqual(self.founders[2:], [self.tom])
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete(self):
|
|
||||||
"""Verify deleting from dataset works."""
|
|
||||||
|
|
||||||
# Delete from front of object
|
|
||||||
del self.founders[0]
|
|
||||||
self.assertEqual(self.founders[:], [self.george, self.tom])
|
|
||||||
|
|
||||||
# Verify dimensions, width should NOT change
|
|
||||||
self.assertEqual(self.founders.height, 2)
|
|
||||||
self.assertEqual(self.founders.width, 3)
|
|
||||||
|
|
||||||
# Delete from back of object
|
|
||||||
del self.founders[1]
|
|
||||||
self.assertEqual(self.founders[:], [self.george])
|
|
||||||
|
|
||||||
# Verify dimensions, width should NOT change
|
|
||||||
self.assertEqual(self.founders.height, 1)
|
|
||||||
self.assertEqual(self.founders.width, 3)
|
|
||||||
|
|
||||||
# Delete from invalid index
|
|
||||||
self.assertRaises(IndexError, self.founders.__delitem__, 3)
|
|
||||||
|
|
||||||
|
|
||||||
def test_csv_export(self):
|
|
||||||
"""Verify exporting dataset object as CSV."""
|
|
||||||
|
|
||||||
# Build up the csv string with headers first, followed by each row
|
|
||||||
csv = ''
|
|
||||||
for col in self.headers:
|
|
||||||
csv += col + ','
|
|
||||||
|
|
||||||
csv = csv.strip(',') + '\r\n'
|
|
||||||
|
|
||||||
for founder in self.founders:
|
|
||||||
for col in founder:
|
|
||||||
csv += str(col) + ','
|
|
||||||
csv = csv.strip(',') + '\r\n'
|
|
||||||
|
|
||||||
self.assertEqual(csv, self.founders.csv)
|
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_append(self):
|
|
||||||
"""Passes in a single unicode charecter and exports."""
|
|
||||||
|
|
||||||
new_row = ('å', 'é')
|
|
||||||
data.append(new_row)
|
|
||||||
|
|
||||||
data.json
|
|
||||||
data.yaml
|
|
||||||
data.csv
|
|
||||||
data.xls
|
|
||||||
|
|
||||||
|
|
||||||
def test_book_export_no_exceptions(self):
|
|
||||||
"""Test that varoius exports don't error out."""
|
|
||||||
|
|
||||||
book = tablib.Databook()
|
|
||||||
book.add_sheet(data)
|
|
||||||
|
|
||||||
book.json
|
|
||||||
book.yaml
|
|
||||||
book.xls
|
|
||||||
|
|
||||||
|
|
||||||
def test_json_import_set(self):
|
|
||||||
"""Generate and import JSON set serialization."""
|
|
||||||
data.append(self.john)
|
|
||||||
data.append(self.george)
|
|
||||||
data.headers = self.headers
|
|
||||||
|
|
||||||
_json = data.json
|
|
||||||
|
|
||||||
data.json = _json
|
|
||||||
|
|
||||||
self.assertEqual(_json, data.json)
|
|
||||||
|
|
||||||
|
|
||||||
def test_json_import_book(self):
|
|
||||||
"""Generate and import JSON book serialization."""
|
|
||||||
data.append(self.john)
|
|
||||||
data.append(self.george)
|
|
||||||
data.headers = self.headers
|
|
||||||
|
|
||||||
book.add_sheet(data)
|
|
||||||
_json = book.json
|
|
||||||
|
|
||||||
book.json = _json
|
|
||||||
|
|
||||||
self.assertEqual(_json, book.json)
|
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_import_set(self):
|
|
||||||
"""Generate and import YAML set serialization."""
|
|
||||||
data.append(self.john)
|
|
||||||
data.append(self.george)
|
|
||||||
data.headers = self.headers
|
|
||||||
|
|
||||||
_yaml = data.yaml
|
|
||||||
|
|
||||||
data.yaml = _yaml
|
|
||||||
|
|
||||||
self.assertEqual(_yaml, data.yaml)
|
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_import_book(self):
|
|
||||||
"""Generate and import YAML book serialization."""
|
|
||||||
data.append(self.john)
|
|
||||||
data.append(self.george)
|
|
||||||
data.headers = self.headers
|
|
||||||
|
|
||||||
book.add_sheet(data)
|
|
||||||
_yaml = book.yaml
|
|
||||||
|
|
||||||
book.yaml = _yaml
|
|
||||||
|
|
||||||
self.assertEqual(_yaml, book.yaml)
|
|
||||||
|
|
||||||
|
|
||||||
def test_csv_import_set(self):
|
|
||||||
"""Generate and import CSV set serialization."""
|
|
||||||
data.append(self.john)
|
|
||||||
data.append(self.george)
|
|
||||||
data.headers = self.headers
|
|
||||||
|
|
||||||
_csv = data.csv
|
|
||||||
|
|
||||||
data.csv = _csv
|
|
||||||
|
|
||||||
self.assertEqual(_csv, data.csv)
|
|
||||||
|
|
||||||
|
|
||||||
def test_wipe(self):
|
|
||||||
"""Purge a dataset."""
|
|
||||||
|
|
||||||
new_row = (1, 2, 3)
|
|
||||||
data.append(new_row)
|
|
||||||
|
|
||||||
# Verify width/data
|
|
||||||
self.assertTrue(data.width == len(new_row))
|
|
||||||
self.assertTrue(data[0] == new_row)
|
|
||||||
|
|
||||||
data.wipe()
|
|
||||||
new_row = (1, 2, 3, 4)
|
|
||||||
data.append(new_row)
|
|
||||||
self.assertTrue(data.width == len(new_row))
|
|
||||||
self.assertTrue(data[0] == new_row)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
MarkupPy
|
||||||
|
odfpy
|
||||||
|
openpyxl>=2.6.0
|
||||||
|
pandas
|
||||||
|
pyyaml
|
||||||
|
tabulate
|
||||||
|
xlrd
|
||||||
|
xlwt
|
||||||
Executable
+1369
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Tests for tablib.packages.dbfpy."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from tablib.packages.dbfpy import fields
|
||||||
|
|
||||||
|
|
||||||
|
class DbfFieldDefTestCompareCase(unittest.TestCase):
|
||||||
|
"""dbfpy.fields.DbfFieldDef comparison test cases, via child classes."""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.length = 10
|
||||||
|
self.a = fields.DbfCharacterFieldDef("abc", self.length)
|
||||||
|
self.z = fields.DbfCharacterFieldDef("xyz", self.length)
|
||||||
|
self.a2 = fields.DbfCharacterFieldDef("abc", self.length)
|
||||||
|
|
||||||
|
def test_compare__eq__(self):
|
||||||
|
# Act / Assert
|
||||||
|
self.assertEqual(self.a, self.a2)
|
||||||
|
|
||||||
|
def test_compare__ne__(self):
|
||||||
|
# Act / Assert
|
||||||
|
self.assertNotEqual(self.a, self.z)
|
||||||
|
|
||||||
|
def test_compare__lt__(self):
|
||||||
|
# Act / Assert
|
||||||
|
self.assertLess(self.a, self.z)
|
||||||
|
|
||||||
|
def test_compare__le__(self):
|
||||||
|
# Act / Assert
|
||||||
|
self.assertLessEqual(self.a, self.a2)
|
||||||
|
self.assertLessEqual(self.a, self.z)
|
||||||
|
|
||||||
|
def test_compare__gt__(self):
|
||||||
|
# Act / Assert
|
||||||
|
self.assertGreater(self.z, self.a)
|
||||||
|
|
||||||
|
def test_compare__ge__(self):
|
||||||
|
# Act / Assert
|
||||||
|
self.assertGreaterEqual(self.a2, self.a)
|
||||||
|
self.assertGreaterEqual(self.z, self.a)
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Tests for tablib.packages.dbfpy."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from tablib.packages.dbfpy import utils
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsUnzfillTestCase(unittest.TestCase):
|
||||||
|
"""dbfpy.utils.unzfill test cases."""
|
||||||
|
|
||||||
|
def test_unzfill_with_nul(self):
|
||||||
|
# Arrange
|
||||||
|
text = b"abc\0xyz"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.unzfill(text)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertEqual(output, b"abc")
|
||||||
|
|
||||||
|
def test_unzfill_without_nul(self):
|
||||||
|
# Arrange
|
||||||
|
text = b"abcxyz"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.unzfill(text)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertEqual(output, b"abcxyz")
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsGetDateTestCase(unittest.TestCase):
|
||||||
|
"""dbfpy.utils.getDate test cases."""
|
||||||
|
|
||||||
|
def test_getDate_none(self):
|
||||||
|
# Arrange
|
||||||
|
value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDate(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
|
||||||
|
def test_getDate_datetime_date(self):
|
||||||
|
# Arrange
|
||||||
|
value = datetime.date(2019, 10, 19)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDate(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, value)
|
||||||
|
|
||||||
|
def test_getDate_datetime_datetime(self):
|
||||||
|
# Arrange
|
||||||
|
value = datetime.datetime(2019, 10, 19, 12, 00, 00)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDate(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, value)
|
||||||
|
|
||||||
|
def test_getDate_datetime_timestamp(self):
|
||||||
|
# Arrange
|
||||||
|
value = 1571515306
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDate(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, datetime.date(2019, 10, 19))
|
||||||
|
|
||||||
|
def test_getDate_datetime_string_yyyy_mm_dd(self):
|
||||||
|
# Arrange
|
||||||
|
value = "20191019"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDate(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, datetime.date(2019, 10, 19))
|
||||||
|
|
||||||
|
def test_getDate_datetime_string_yymmdd(self):
|
||||||
|
# Arrange
|
||||||
|
value = "191019"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDate(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, datetime.date(2019, 10, 19))
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsGetDateTimeTestCase(unittest.TestCase):
|
||||||
|
"""dbfpy.utils.getDateTime test cases."""
|
||||||
|
|
||||||
|
def test_getDateTime_none(self):
|
||||||
|
# Arrange
|
||||||
|
value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDateTime(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.datetime)
|
||||||
|
|
||||||
|
def test_getDateTime_datetime_datetime(self):
|
||||||
|
# Arrange
|
||||||
|
value = datetime.datetime(2019, 10, 19, 12, 00, 00)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDateTime(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, value)
|
||||||
|
|
||||||
|
def test_getDateTime_datetime_date(self):
|
||||||
|
# Arrange
|
||||||
|
value = datetime.date(2019, 10, 19)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDateTime(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.date)
|
||||||
|
self.assertEqual(output, datetime.datetime(2019, 10, 19, 00, 00))
|
||||||
|
|
||||||
|
def test_getDateTime_datetime_timestamp(self):
|
||||||
|
# Arrange
|
||||||
|
value = 1571515306
|
||||||
|
|
||||||
|
# Act
|
||||||
|
output = utils.getDateTime(value)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsInstance(output, datetime.datetime)
|
||||||
|
|
||||||
|
def test_getDateTime_datetime_string(self):
|
||||||
|
# Arrange
|
||||||
|
value = "20191019"
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
output = utils.getDateTime(value)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidValueTestCase(unittest.TestCase):
|
||||||
|
"""dbfpy.utils._InvalidValue test cases."""
|
||||||
|
|
||||||
|
def test_sanity(self):
|
||||||
|
# Arrange
|
||||||
|
INVALID_VALUE = utils.INVALID_VALUE
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
self.assertEqual(INVALID_VALUE, INVALID_VALUE)
|
||||||
|
self.assertNotEqual(INVALID_VALUE, 123)
|
||||||
|
self.assertEqual(int(INVALID_VALUE), 0)
|
||||||
|
self.assertEqual(float(INVALID_VALUE), 0.0)
|
||||||
|
self.assertEqual(str(INVALID_VALUE), "")
|
||||||
|
self.assertEqual(repr(INVALID_VALUE), "<INVALID>")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
[tox]
|
||||||
|
usedevelop = true
|
||||||
|
minversion = 2.4
|
||||||
|
envlist =
|
||||||
|
docs
|
||||||
|
lint
|
||||||
|
py{36,37,38,39}
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
deps =
|
||||||
|
-rtests/requirements.txt
|
||||||
|
extras = pandas
|
||||||
|
passenv =
|
||||||
|
FORCE_COLOR
|
||||||
|
commands =
|
||||||
|
pytest {posargs:tests}
|
||||||
|
|
||||||
|
[testenv:docs]
|
||||||
|
deps =
|
||||||
|
sphinx
|
||||||
|
commands =
|
||||||
|
sphinx-build -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
|
||||||
|
|
||||||
|
[testenv:lint]
|
||||||
|
deps =
|
||||||
|
flake8
|
||||||
|
# flake8-black
|
||||||
|
pre-commit
|
||||||
|
twine
|
||||||
|
commands =
|
||||||
|
# flake8 src/tablib tests/
|
||||||
|
pre-commit run --all-files
|
||||||
|
python setup.py sdist
|
||||||
|
twine check dist/*
|
||||||
|
skip_install = true
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
exclude =
|
||||||
|
.tox
|
||||||
Reference in New Issue
Block a user