diff --git a/Pipfile b/Pipfile index 97e1f28d..f6f66b28 100644 --- a/Pipfile +++ b/Pipfile @@ -19,6 +19,7 @@ pytz = "==2018.7" "flake8" = "*" coverage = "*" ipython = "*" +autopep8 = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index c1c0b2d1..09fda139 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "856055e62dd82d4a1f853cc76716f813504e41b4c198c3b6ba988e248a156b44" + "sha256": "7eeb23c826a7cad627b63d097d320d7135c3f2d2159800a2d62a13a795fa4428" }, "pipfile-spec": 6, "requires": { @@ -18,10 +18,10 @@ "default": { "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" ], - "version": "==2018.11.29" + "version": "==2019.3.9" }, "chardet": { "hashes": [ @@ -100,43 +100,43 @@ }, "jinja2": { "hashes": [ - "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", - "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", + "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" ], - "version": "==2.10" + "version": "==2.10.1" }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" - ], - "version": "==1.1.0" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" }, "python-decouple": { "hashes": [ @@ -171,10 +171,10 @@ }, "urllib3": { "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" ], - "version": "==1.24.1" + "version": "==1.24.3" }, "whitenoise": { "hashes": [ @@ -186,6 +186,13 @@ } }, "develop": { + "autopep8": { + "hashes": [ + "sha256:4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee" + ], + "index": "pypi", + "version": "==1.4.4" + }, "backcall": { "hashes": [ "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", @@ -195,63 +202,80 @@ }, "coverage": { "hashes": [ - "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", - "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", - "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", - "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", - "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", - "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", - "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", - "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", - "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", - "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", - "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", - "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", - "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", - "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", - "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", - "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", - "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", - "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", - "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", - "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", - "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", - "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", - "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", - "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", - "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", - "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", - "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", - "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", - "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", - "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", - "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + "sha256:0c5fe441b9cfdab64719f24e9684502a59432df7570521563d7b1aff27ac755f", + "sha256:2b412abc4c7d6e019ce7c27cbc229783035eef6d5401695dccba80f481be4eb3", + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:42692db854d13c6c5e9541b6ffe0fe921fe16c9c446358d642ccae1462582d3b", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:4ec30ade438d1711562f3786bea33a9da6107414aed60a5daa974d50a8c2c351", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:6899797ac384b239ce1926f3cb86ffc19996f6fa3a1efbb23cb49e0c12d8c18c", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8e679d1bde5e2de4a909efb071f14b472a678b788904440779d2c449c0355b27", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:93f965415cc51604f571e491f280cff0f5be35895b4eb5e55b47ae90c02a497b", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:a9abc8c480e103dc05d9b332c6cc9fb1586330356fc14f1aa9c0ca5745097d19", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c22ab9f96cbaff05c6a84e20ec856383d27eae09e511d3e6ac4479489195861d", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:ca58eba39c68010d7e87a823f22a081b5290e3e3c64714aac3c91481d8b34d22", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" ], "index": "pypi", - "version": "==4.5.2" + "version": "==4.5.3" }, "decorator": { "hashes": [ - "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", - "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", + "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" ], - "version": "==4.3.0" + "version": "==4.4.0" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" }, "flake8": { "hashes": [ - "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", - "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], "index": "pypi", - "version": "==3.6.0" + "version": "==3.7.7" }, "ipython": { "hashes": [ - "sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12", - "sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742" + "sha256:54c5a8aa1eadd269ac210b96923688ccf01ebb2d0f21c18c3c717909583579a8", + "sha256:e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26" ], "index": "pypi", - "version": "==7.2.0" + "version": "==7.5.0" }, "ipython-genutils": { "hashes": [ @@ -262,10 +286,10 @@ }, "jedi": { "hashes": [ - "sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd", - "sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191" + "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b", + "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c" ], - "version": "==0.13.2" + "version": "==0.13.3" }, "mccabe": { "hashes": [ @@ -276,18 +300,18 @@ }, "parso": { "hashes": [ - "sha256:4b8f9ed80c3a4a3191aa3261505d868aa552dd25649cb13a7d73b6b7315edf2d", - "sha256:5a120be2e8863993b597f1c0437efca799e90e0793c98ae5d4e34ebd00140e31" + "sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33", + "sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376" ], - "version": "==0.3.2" + "version": "==0.4.0" }, "pexpect": { "hashes": [ - "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", - "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", + "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb" ], "markers": "sys_platform != 'win32'", - "version": "==4.6.0" + "version": "==4.7.0" }, "pickleshare": { "hashes": [ @@ -298,11 +322,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34", - "sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9", - "sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39" + "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", + "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", + "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" ], - "version": "==2.0.7" + "version": "==2.0.9" }, "ptyprocess": { "hashes": [ @@ -313,17 +337,17 @@ }, "pycodestyle": { "hashes": [ - "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", - "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" ], - "version": "==2.4.0" + "version": "==2.5.0" }, "pyflakes": { "hashes": [ - "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", - "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" ], - "version": "==2.0.0" + "version": "==2.1.1" }, "pygments": { "hashes": [ diff --git a/bothub/api/v1/serializers/__init__.py b/bothub/api/v1/serializers/__init__.py index ef032af5..aed2df0d 100644 --- a/bothub/api/v1/serializers/__init__.py +++ b/bothub/api/v1/serializers/__init__.py @@ -3,6 +3,7 @@ RepositorySerializer, RepositoryAuthorizationSerializer, AnalyzeTextSerializer, + EvaluateSerializer, EditRepositorySerializer, VoteSerializer, RepositoryAuthorizationRoleSerializer, diff --git a/bothub/api/v1/serializers/repository.py b/bothub/api/v1/serializers/repository.py index 857a6569..30fe5c1b 100644 --- a/bothub/api/v1/serializers/repository.py +++ b/bothub/api/v1/serializers/repository.py @@ -186,6 +186,10 @@ class AnalyzeTextSerializer(serializers.Serializer): text = serializers.CharField(allow_blank=False) +class EvaluateSerializer(serializers.Serializer): + language = serializers.ChoiceField(LANGUAGE_CHOICES, required=True) + + class VoteSerializer(serializers.ModelSerializer): class Meta: model = RepositoryVote diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 18538fd0..b9d3a903 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -45,6 +45,7 @@ from .serializers import RepositoryCategorySerializer from .serializers import NewRepositoryExampleSerializer from .serializers import AnalyzeTextSerializer +from .serializers import EvaluateSerializer from .serializers import EditRepositorySerializer from .serializers import NewRepositoryTranslatedExampleSerializer from .serializers import VoteSerializer @@ -523,6 +524,40 @@ def analyze(self, request, **kwargs): message = error.get('message') # pragma: no cover raise APIException(detail=message) # pragma: no cover + @detail_route( + methods=['POST'], + url_name='repository-evaluate') + def evaluate(self, request, **kwargs): + """ + Evaluate repository using Bothub NLP service + """ + repository = self.get_object() + user_authorization = repository.get_user_authorization(request.user) + if not user_authorization.can_write: + raise PermissionDenied() + serializer = EvaluateSerializer( + data=request.data) # pragma: no cover + serializer.is_valid(raise_exception=True) # pragma: no cover + + if not repository.evaluations( + language=request.data.get('language')).count(): + raise APIException( + detail=_('You need to have at least ' + + 'one registered test phrase')) # pragma: no cover + + if len(repository.intents) <= 1: + raise APIException( + detail=_('You need to have at least ' + + 'two registered intents')) # pragma: no cover + + request = Repository.request_nlp_evaluate( # pragma: no cover + user_authorization, serializer.data) + if request.status_code != status.HTTP_200_OK: # pragma: no cover + raise APIException( # pragma: no cover + {'status_code': request.status_code}, + code=request.status_code) + return Response(request.json()) # pragma: no cover + @detail_route( methods=['POST'], url_name='repository-vote', @@ -552,7 +587,7 @@ def vote(self, request, **kwargs): def get_serializer_class(self): if self.request and self.request.method in \ ['OPTIONS'] + WRITE_METHODS or not self.request: - return self.edit_serializer_class + return self.edit_serializer_class return self.serializer_class def get_action_permissions_classes(self): diff --git a/bothub/api/v2/evaluate/__init__.py b/bothub/api/v2/evaluate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/evaluate/filters.py b/bothub/api/v2/evaluate/filters.py new file mode 100644 index 00000000..e1fa4044 --- /dev/null +++ b/bothub/api/v2/evaluate/filters.py @@ -0,0 +1,135 @@ +from django.utils.translation import gettext as _ +from django.core.exceptions import ValidationError as DjangoValidationError +from django_filters import rest_framework as filters + +from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import NotFound + +from bothub.common.models import Repository +from bothub.common.models import RepositoryEvaluate +from bothub.common.models import RepositoryEvaluateResult + + +class EvaluatesFilter(filters.FilterSet): + + class Meta: + model = RepositoryEvaluate + fields = [ + 'text', + 'language', + 'intent', + ] + + repository_uuid = filters.CharFilter( + field_name='repository_uuid', + method='filter_repository_uuid', + required=True, + help_text=_('Repository\'s UUID')) + + language = filters.CharFilter( + field_name='language', + method='filter_language', + help_text='Filter by language, default is repository base language') + + label = filters.CharFilter( + field_name='label', + method='filter_label', + help_text=_( + 'Filter evaluations with entities with a specific label.' + ) + ) + + entity = filters.CharFilter( + field_name='entity', + method='filter_entity', + help_text=_('Filter evaluations with an entity.')) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + if not authorization.can_read: + raise PermissionDenied() + return repository.evaluations(queryset=queryset) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository_uuid')) + + def filter_language(self, queryset, name, value): + return queryset.filter(repository_update__language=value) + + def filter_label(self, queryset, name, value): + if value == 'other': + return queryset.filter(entities__entity__label__isnull=True) + return queryset.filter(entities__entity__label__value=value) + + def filter_entity(self, queryset, name, value): + return queryset.filter(entities__entity__value=value) + + +class EvaluateResultsFilter(filters.FilterSet): + + class Meta: + model = RepositoryEvaluateResult + fields = [] + + repository_uuid = filters.CharFilter( + field_name='repository_uuid', + method='filter_repository_uuid', + required=True, + help_text=_('Repository\'s UUID')) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + + if not authorization.can_read: + raise PermissionDenied() + return repository.evaluations_results(queryset=queryset) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository_uuid')) + + +class EvaluateResultFilter(filters.FilterSet): + + class Meta: + model = RepositoryEvaluateResult + fields = [] + + text = filters.CharFilter( + field_name='text', + method='filter_evaluate_text', + required=False, + help_text=_('Evaluate Text')) + + repository_uuid = filters.CharFilter( + field_name='repository_uuid', + method='filter_repository_uuid', + required=True, + help_text=_('Repository\'s UUID')) + + def filter_evaluate_text(self, queryset, name, value): + return queryset.filter(log__icontains=value) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + + if not authorization.can_read: + raise PermissionDenied() + return repository.evaluations_results(queryset=queryset) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository_uuid')) diff --git a/bothub/api/v2/evaluate/permissions.py b/bothub/api/v2/evaluate/permissions.py new file mode 100644 index 00000000..79fbb631 --- /dev/null +++ b/bothub/api/v2/evaluate/permissions.py @@ -0,0 +1,29 @@ +from rest_framework import permissions + +from .. import READ_METHODS +from .. import WRITE_METHODS + + +class RepositoryEvaluatePermission(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + authorization = obj.repository_update. \ + repository.get_user_authorization(request.user) + if request.method in READ_METHODS: + return authorization.can_read + if request.user.is_authenticated: + if request.method in WRITE_METHODS: + return authorization.can_write + return authorization.is_admin + return False + + +class RepositoryEvaluateResultPermission(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + authorization = obj.repository_update. \ + repository.get_user_authorization(request.user) + + if request.method in READ_METHODS: + return authorization.can_read + return authorization.can_contribute diff --git a/bothub/api/v2/evaluate/serializers.py b/bothub/api/v2/evaluate/serializers.py new file mode 100644 index 00000000..3c9213ed --- /dev/null +++ b/bothub/api/v2/evaluate/serializers.py @@ -0,0 +1,195 @@ +import json + +from django.utils.translation import gettext as _ +from rest_framework import serializers + +from bothub.common.models import Repository +from bothub.common.models import RepositoryEvaluate +from bothub.common.models import RepositoryEvaluateEntity +from bothub.common.models import RepositoryEvaluateResult +from bothub.common.models import RepositoryEvaluateResultScore +from bothub.common.models import RepositoryEvaluateResultIntent +from bothub.common.models import RepositoryEvaluateResultEntity + +from bothub.common.languages import LANGUAGE_CHOICES + +from ..fields import EntityValueField +from .validators import ThereIsEntityValidator +from .validators import ThereIsIntentValidator + + +class RepositoryEvaluateEntitySerializer(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluateEntity + fields = [ + 'entity', + 'start', + 'end', + ] + + entity = EntityValueField() + + +class RepositoryEvaluateSerializer(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluate + fields = [ + 'id', + 'repository', + 'text', + 'language', + 'intent', + 'entities', + 'created_at', + ] + read_only_fields = [ + 'deleted_in', + 'created_at', + ] + + entities = RepositoryEvaluateEntitySerializer( + many=True, + required=False, + ) + + repository = serializers.PrimaryKeyRelatedField( + queryset=Repository.objects, + write_only=True, + required=True, + ) + + language = serializers.ChoiceField( + LANGUAGE_CHOICES, + label=_('Language') + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators.append(ThereIsEntityValidator()) + self.validators.append(ThereIsIntentValidator()) + + def create(self, validated_data): + entities = validated_data.pop('entities') + repository = validated_data.pop('repository') + language = validated_data.pop('language') + + repository_update = repository.current_update(language) + validated_data.update({'repository_update': repository_update}) + evaluate = RepositoryEvaluate.objects.create(**validated_data) + + for entity in entities: + RepositoryEvaluateEntity.objects.create( + repository_evaluate=evaluate, **entity) + + return evaluate + + def update(self, instance, validated_data): + repository = validated_data.pop('repository') + language = validated_data.get('language', instance.language) + + instance.text = validated_data.get('text', instance.text) + instance.intent = validated_data.get('intent', instance.intent) + instance.repository_update = repository.current_update(language) + instance.save() + instance.delete_entities() + + for entity in validated_data.pop('entities'): + RepositoryEvaluateEntity.objects.create( + repository_evaluate=instance, **entity) + + return instance + + +class RepositoryEvaluateResultVersionsSerializer(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluateResult + fields = [ + 'id', + 'language', + 'created_at', + 'version', + ] + + language = serializers.SerializerMethodField() + + def get_language(self, obj): + return obj.repository_update.language + + +class RepositoryEvaluateResultScore(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluateResultScore + fields = [ + 'precision', + 'f1_score', + 'accuracy', + 'recall', + 'support' + ] + + +class RepositoryEvaluateResultIntentSerializer(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluateResultIntent + fields = [ + 'intent', + 'score', + ] + + score = RepositoryEvaluateResultScore(read_only=True) + + +class RepositoryEvaluateResultEntitySerializer(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluateResultEntity + fields = [ + 'entity', + 'score', + ] + + score = RepositoryEvaluateResultScore(read_only=True) + entity = serializers.SerializerMethodField() + + def get_entity(self, obj): + return obj.entity.value + + +class RepositoryEvaluateResultSerializer(serializers.ModelSerializer): + + class Meta: + model = RepositoryEvaluateResult + fields = [ + 'id', + 'created_at', + 'matrix_chart', + 'confidence_chart', + 'log', + 'intents_list', + 'entities_list', + 'intent_results', + 'entity_results' + + ] + + log = serializers.SerializerMethodField() + intents_list = serializers.SerializerMethodField() + entities_list = serializers.SerializerMethodField() + intent_results = RepositoryEvaluateResultScore(read_only=True) + entity_results = RepositoryEvaluateResultScore(read_only=True) + + def get_intents_list(self, obj): + return RepositoryEvaluateResultIntentSerializer( + obj.evaluate_result_intent.all(), many=True).data + + def get_entities_list(self, obj): + return RepositoryEvaluateResultEntitySerializer( + obj.evaluate_result_entity.all(), many=True).data + + def get_log(self, obj): + return json.loads(obj.log) diff --git a/bothub/api/v2/evaluate/validators.py b/bothub/api/v2/evaluate/validators.py new file mode 100644 index 00000000..371cb1ef --- /dev/null +++ b/bothub/api/v2/evaluate/validators.py @@ -0,0 +1,29 @@ +from django.utils.translation import gettext as _ +from rest_framework.exceptions import ValidationError + + +class ThereIsIntentValidator(object): + + def __call__(self, attrs): + if attrs.get('intent') not in attrs.get('repository').intents: + raise ValidationError(_( + 'Intent MUST match existing intents for training.' + )) + + +class ThereIsEntityValidator(object): + + def __call__(self, attrs): + entities = attrs.get('entities') + repository = attrs.get('repository') + + if entities: + entities_list = list(set( + map(lambda x: x.get('entity'), attrs.get('entities')))) + repository_entities_list = repository.entities.filter( + value__in=entities_list) + + if len(entities_list) != len(repository_entities_list): + raise ValidationError({'entities': _( + 'Entities MUST match existing entities for training.' + )}) diff --git a/bothub/api/v2/evaluate/views.py b/bothub/api/v2/evaluate/views.py new file mode 100644 index 00000000..1987872c --- /dev/null +++ b/bothub/api/v2/evaluate/views.py @@ -0,0 +1,85 @@ +from rest_framework.viewsets import GenericViewSet +from rest_framework import mixins +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.filters import SearchFilter +from rest_framework.filters import OrderingFilter + +from django_filters.rest_framework import DjangoFilterBackend + +from bothub.common.models import RepositoryEvaluate +from bothub.common.models import RepositoryEvaluateResult + +from ..metadata import Metadata +from .serializers import RepositoryEvaluateSerializer +from .serializers import RepositoryEvaluateResultVersionsSerializer +from .serializers import RepositoryEvaluateResultSerializer + +from .filters import EvaluatesFilter +from .filters import EvaluateResultsFilter +from .filters import EvaluateResultFilter + +from .permissions import RepositoryEvaluatePermission +from .permissions import RepositoryEvaluateResultPermission + + +class EvaluateViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet): + """ + Manager evaluate (tests). + """ + queryset = RepositoryEvaluate.objects + serializer_class = RepositoryEvaluateSerializer + permission_classes = [ + IsAuthenticatedOrReadOnly, + RepositoryEvaluatePermission, + ] + metadata_class = Metadata + + def list(self, request, *args, **kwargs): + self.filter_class = EvaluatesFilter + self.filter_backends = [ + OrderingFilter, + SearchFilter, + DjangoFilterBackend, + ] + self.search_fields = [ + '$text', + '^text', + '=text', + ] + self.ordering_fields = [ + 'created_at', + ] + + return super().list(request, *args, **kwargs) + + +class ResultsListViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + GenericViewSet): + + queryset = RepositoryEvaluateResult.objects + serializer_class = RepositoryEvaluateResultVersionsSerializer + permission_classes = [ + IsAuthenticatedOrReadOnly, + RepositoryEvaluateResultPermission, + ] + filter_class = EvaluateResultsFilter + filter_backends = [ + OrderingFilter, + DjangoFilterBackend, + ] + ordering_fields = [ + 'created_at', + ] + + def retrieve(self, request, *args, **kwargs): + self.serializer_class = RepositoryEvaluateResultSerializer + self.filter_class = EvaluateResultFilter + return super().retrieve(request, *args, **kwargs) diff --git a/bothub/api/v2/fields.py b/bothub/api/v2/fields.py new file mode 100644 index 00000000..b41552d3 --- /dev/null +++ b/bothub/api/v2/fields.py @@ -0,0 +1,56 @@ +from rest_framework import serializers + +from bothub.common.models import RepositoryEntity +from bothub.common.models import RepositoryEntityLabel + + +class ModelMultipleChoiceField(serializers.ManyRelatedField): + pass + + +class TextField(serializers.CharField): + pass + + +class PasswordField(serializers.CharField): + def __init__(self, *args, **kwargs): + kwargs.pop('trim_whitespace', None) + super().__init__(trim_whitespace=False, **kwargs) + + +class EntityText(serializers.CharField): + pass + + +class EntityValueField(serializers.CharField): + def __init__(self, *args, validators=[], **kwargs): + kwargs.pop('max_length', 0) + kwargs.pop('help_text', '') + + value_field = RepositoryEntity._meta.get_field('value') + + super().__init__( + *args, + max_length=value_field.max_length, + validators=(validators + value_field.validators), + **kwargs) + + def to_representation(self, obj): + return obj.value + + +class LabelValueField(serializers.CharField): + def __init__(self, *args, validators=[], **kwargs): + kwargs.pop('max_length', 0) + kwargs.pop('help_text', '') + + value_field = RepositoryEntityLabel._meta.get_field('value') + + super().__init__( + *args, + max_length=value_field.max_length, + validators=(validators + value_field.validators), + **kwargs) + + def to_representation(self, obj): + return obj.value diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index fea78e29..af02e274 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -91,6 +91,7 @@ class Meta: 'description', 'is_private', 'available_languages', + 'entities', 'entities_list', 'labels_list', 'ready_for_train', @@ -105,6 +106,7 @@ class Meta: 'labels', 'other_label', 'examples__count', + 'evaluate_languages_count', 'absolute_url', 'authorization', 'ready_for_train', @@ -121,7 +123,9 @@ class Meta: read_only = [ 'uuid', 'available_languages', + 'entities', 'entities_list', + 'evaluate_languages_count', 'labels_list', 'ready_for_train', 'created_at', @@ -152,10 +156,12 @@ class Meta: read_only=True) other_label = serializers.SerializerMethodField() examples__count = serializers.SerializerMethodField() + evaluate_languages_count = serializers.SerializerMethodField() absolute_url = serializers.SerializerMethodField() authorization = serializers.SerializerMethodField() request_authorization = serializers.SerializerMethodField() available_request_authorization = serializers.SerializerMethodField() + entities = serializers.SerializerMethodField() def create(self, validated_data): validated_data.update({ @@ -163,6 +169,9 @@ def create(self, validated_data): }) return super().create(validated_data) + def get_entities(self, obj): + return obj.current_entities.values('value', 'id').distinct() + def get_intents(self, obj): return IntentSerializer( map( @@ -187,6 +196,12 @@ def get_other_label(self, obj): def get_examples__count(self, obj): return obj.examples().count() + def get_evaluate_languages_count(self, obj): + return dict(map( + lambda x: (x, obj.evaluations(language=x).count() + ), obj.available_languages + )) + def get_absolute_url(self, obj): return obj.get_absolute_url() diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 03e294bf..45bfc546 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -3,9 +3,13 @@ from .repository.views import RepositoryViewSet from .repository.views import RepositoriesViewSet from .examples.views import ExamplesViewSet +from .evaluate.views import EvaluateViewSet +from .evaluate.views import ResultsListViewSet router = routers.SimpleRouter() router.register('repository', RepositoryViewSet) router.register('repositories', RepositoriesViewSet) router.register('examples', ExamplesViewSet) +router.register('evaluate/results', ResultsListViewSet) +router.register('evaluate', EvaluateViewSet) diff --git a/bothub/common/management/commands/fill_db_using_fake_data.py b/bothub/common/management/commands/fill_db_using_fake_data.py index c1df595b..7fe1cef4 100644 --- a/bothub/common/management/commands/fill_db_using_fake_data.py +++ b/bothub/common/management/commands/fill_db_using_fake_data.py @@ -1,4 +1,5 @@ import random +import json from django.core.management.base import BaseCommand from django.conf import settings @@ -10,6 +11,13 @@ from bothub.common.models import RepositoryExampleEntity from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryTranslatedExampleEntity +from bothub.common.models import RepositoryEvaluate +from bothub.common.models import RepositoryEvaluateEntity +from bothub.common.models import RepositoryEvaluateResult +from bothub.common.models import RepositoryEvaluateResultScore +from bothub.common.models import RepositoryEvaluateResultIntent +from bothub.common.models import RepositoryEvaluateResultEntity + from bothub.common import languages @@ -100,6 +108,12 @@ def handle(self, *args, **kwargs): # Example Entity + RepositoryExampleEntity.objects.create( + repository_example=example_1, + start=8, + end=15, + entity='cuisine') + RepositoryExampleEntity.objects.create( repository_example=example_5, start=8, @@ -135,3 +149,179 @@ def handle(self, *args, **kwargs): start=23, end=29, entity='cuisine') + + # Evaluates + + evalute_1 = RepositoryEvaluate.objects.create( + repository_update=repository_1.current_update(), + text='show me chinese restaurants', + intent='restaurant_search') + + evalute_2 = RepositoryEvaluate.objects.create( + repository_update=repository_1.current_update(), + text='hello', + intent='greet') + + RepositoryEvaluate.objects.create( + repository_update=repository_1.current_update(), + text='yes', + intent='affirm') + + RepositoryEvaluate.objects.create( + repository_update=repository_1.current_update(), + text='yep', + intent='affirm') + + RepositoryEvaluateEntity.objects.create( + repository_evaluate=evalute_1, + start=23, + end=29, + entity='cuisine') + + RepositoryEvaluateEntity.objects.create( + repository_evaluate=evalute_2, + start=0, + end=5, + entity='greet') + + # Evaluate Report + + for x in range(0, 2): + intent_results = RepositoryEvaluateResultScore.objects.create( + f1_score=0.976, + precision=0.978, + accuracy=0.976, + ) + + entity_results = RepositoryEvaluateResultScore.objects.create( + f1_score=0.977, + precision=0.978, + accuracy=0.978, + ) + + evaluate_log = [ + { + "text": "hey", + "intent": "greet", + "intent_prediction": { + "name": "greet", + "confidence": 0.9263743763408538 + }, + "status": "success" + }, + { + "text": "howdy", + "intent": "greet", + "intent_prediction": { + "name": "greet", + "confidence": 0.8099720606047796 + }, + "status": "success" + }, + { + "text": "hey there", + "intent": "greet", + "intent_prediction": { + "name": "greet", + "confidence": 0.8227075176309955 + }, + "status": "success" + }, + { + "text": "test with nlu", + "intent": "restaurant_search", + "intent_prediction": { + "name": "goodbye", + "confidence": 0.3875259420712092 + }, + "status": "error" + } + ] + + sample_url = 'https://s3.amazonaws.com/bothub-sample' + evaluate_result = RepositoryEvaluateResult.objects.create( + repository_update=repository_1.current_update(), + intent_results=intent_results, + entity_results=entity_results, + matrix_chart='{}/confmat.png'.format(sample_url), + confidence_chart='{}/hist.png'.format(sample_url), + log=json.dumps(evaluate_log), + ) + + intent_score_1 = RepositoryEvaluateResultScore.objects.create( + precision=1.0, + recall=1.0, + f1_score=1.0, + support=11, + ) + + intent_score_2 = RepositoryEvaluateResultScore.objects.create( + precision=0.89, + recall=1.0, + f1_score=0.94, + support=8, + ) + + intent_score_3 = RepositoryEvaluateResultScore.objects.create( + precision=1.0, + recall=1.0, + f1_score=1.0, + support=8, + ) + + intent_score_4 = RepositoryEvaluateResultScore.objects.create( + precision=1.0, + recall=0.93, + f1_score=0.97, + support=15, + ) + + RepositoryEvaluateResultIntent.objects.create( + evaluate_result=evaluate_result, + intent='affirm', + score=intent_score_1, + ) + + RepositoryEvaluateResultIntent.objects.create( + evaluate_result=evaluate_result, + intent='goodbye', + score=intent_score_2, + ) + + RepositoryEvaluateResultIntent.objects.create( + evaluate_result=evaluate_result, + intent='greet', + score=intent_score_3, + ) + + RepositoryEvaluateResultIntent.objects.create( + evaluate_result=evaluate_result, + intent='restaurant_search', + score=intent_score_4, + ) + + entity_score_1 = RepositoryEvaluateResultScore.objects.create( + precision=1.0, + recall=0.90, + f1_score=0.95, + support=10, + ) + + entity_score_2 = RepositoryEvaluateResultScore.objects.create( + precision=1.0, + recall=0.75, + f1_score=0.86, + support=8, + ) + + RepositoryEvaluateResultEntity.objects.create( + evaluate_result=evaluate_result, + entity='cuisine', + score=entity_score_1, + ) + + RepositoryEvaluateResultEntity.objects.create( + evaluate_result=evaluate_result, + entity='greet', + score=entity_score_2, + ) diff --git a/bothub/common/migrations/0031_auto_20190502_1732.py b/bothub/common/migrations/0031_auto_20190502_1732.py new file mode 100644 index 00000000..6fbf3161 --- /dev/null +++ b/bothub/common/migrations/0031_auto_20190502_1732.py @@ -0,0 +1,127 @@ +# Generated by Django 2.1.5 on 2019-05-02 17:32 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0030_auto_20190327_2003'), + ] + + operations = [ + migrations.CreateModel( + name='RepositoryEvaluate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField(help_text='Evaluate test text', verbose_name='text')), + ('intent', models.CharField(default='no_intent', help_text='Evaluate intent reference', max_length=64, validators=[django.core.validators.RegexValidator(re.compile('^[-a-z0-9_]+\\Z'), 'Enter a valid value consisting of lowercase letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='intent')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('deleted_in', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='deleted_evaluate', to='common.RepositoryUpdate')), + ('repository_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='added_evaluate', to='common.RepositoryUpdate')), + ], + options={ + 'verbose_name': 'repository evaluate test', + 'verbose_name_plural': 'repository evaluate tests', + 'db_table': 'common_repository_evaluate', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='RepositoryEvaluateEntity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start', models.PositiveIntegerField(help_text='Start index of entity value in example text', verbose_name='start')), + ('end', models.PositiveIntegerField(help_text='End index of entity value in example text', verbose_name='end')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('entity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='common.RepositoryEntity')), + ('repository_evaluate', models.ForeignKey(editable=False, help_text='evaluate object', on_delete=django.db.models.deletion.CASCADE, related_name='entities', to='common.RepositoryEvaluate')), + ], + options={ + 'db_table': 'common_repository_evaluate_entity', + }, + ), + migrations.CreateModel( + name='RepositoryEvaluateResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('matrix_chart', models.URLField(editable=False, verbose_name='Intent Confusion Matrix Chart')), + ('confidence_chart', models.URLField(editable=False, verbose_name='Intent Prediction Confidence Distribution')), + ('log', models.TextField(blank=True, editable=False, verbose_name='Evaluate Log')), + ('version', models.IntegerField(default=0, editable=False, verbose_name='Version')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ], + options={ + 'verbose_name': 'evaluate results', + 'verbose_name_plural': 'evaluate results', + 'db_table': 'common_repository_evaluate_result', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='RepositoryEvaluateResultEntity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('entity', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='entity', to='common.RepositoryEntity')), + ('evaluate_result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evaluate_result_entity', to='common.RepositoryEvaluateResult')), + ], + options={ + 'db_table': 'common_repository_evaluate_result_entity', + }, + ), + migrations.CreateModel( + name='RepositoryEvaluateResultIntent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('intent', models.CharField(help_text='Evaluate intent reference', max_length=64, validators=[django.core.validators.RegexValidator(re.compile('^[-a-z0-9_]+\\Z'), 'Enter a valid value consisting of lowercase letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='intent')), + ('evaluate_result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evaluate_result_intent', to='common.RepositoryEvaluateResult')), + ], + options={ + 'db_table': 'common_repository_evaluate_result_intent', + }, + ), + migrations.CreateModel( + name='RepositoryEvaluateResultScore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('precision', models.DecimalField(decimal_places=2, max_digits=3, null=True)), + ('f1_score', models.DecimalField(decimal_places=2, max_digits=3, null=True)), + ('accuracy', models.DecimalField(decimal_places=2, max_digits=3, null=True)), + ('recall', models.DecimalField(decimal_places=2, max_digits=3, null=True)), + ('support', models.IntegerField(null=True)), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ], + options={ + 'db_table': 'common_repository_evaluate_result_score', + 'ordering': ['-created_at'], + }, + ), + migrations.AddField( + model_name='repositoryevaluateresultintent', + name='score', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='evaluation_intenties_score', to='common.RepositoryEvaluateResultScore'), + ), + migrations.AddField( + model_name='repositoryevaluateresultentity', + name='score', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='evaluation_entities_score', to='common.RepositoryEvaluateResultScore'), + ), + migrations.AddField( + model_name='repositoryevaluateresult', + name='entity_results', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='entity_results', to='common.RepositoryEvaluateResultScore'), + ), + migrations.AddField( + model_name='repositoryevaluateresult', + name='intent_results', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='intent_results', to='common.RepositoryEvaluateResultScore'), + ), + migrations.AddField( + model_name='repositoryevaluateresult', + name='repository_update', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='results', to='common.RepositoryUpdate'), + ), + ] diff --git a/bothub/common/models.py b/bothub/common/models.py index 7d79cd85..6eca13df 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -179,6 +179,7 @@ class Meta: nlp_train_url = '{}train/'.format(settings.BOTHUB_NLP_BASE_URL) nlp_analyze_url = '{}parse/'.format(settings.BOTHUB_NLP_BASE_URL) + nlp_evaluate_url = '{}evaluate/'.format(settings.BOTHUB_NLP_BASE_URL) @classmethod def request_nlp_train(cls, user_authorization): @@ -201,6 +202,17 @@ def request_nlp_analyze(cls, user_authorization, data): user_authorization.uuid)}) return r # pragma: no cover + @classmethod + def request_nlp_evaluate(cls, user_authorization, data): + r = requests.post( # pragma: no cover + cls.nlp_evaluate_url, + data={ + 'language': data.get('language'), + }, + headers={'Authorization': 'Bearer {}'.format( + user_authorization.uuid)}) + return r # pragma: no cover + @property def available_languages(self): examples = self.examples() @@ -335,6 +347,25 @@ def examples(self, language=None, exclude_deleted=True, queryset=None): return query.exclude(deleted_in__isnull=False) return query + def evaluations(self, language=None, exclude_deleted=True, queryset=None): + if queryset is None: + queryset = RepositoryEvaluate.objects + query = queryset.filter( + repository_update__repository=self) + if language: + query = query.filter( + repository_update__language=language) + if exclude_deleted: + return query.exclude(deleted_in__isnull=False) + return query + + def evaluations_results(self, queryset=None): + if queryset is None: + queryset = RepositoryEvaluateResult.objects + query = queryset.filter( + repository_update__repository=self) + return query + def language_status(self, language): is_base_language = self.language == language examples = self.examples(language) @@ -546,7 +577,7 @@ def ready_for_train(self): if self.examples.count() == 0: return False - return len(self.requirements_to_train) is 0 + return len(self.requirements_to_train) == 0 @property def intents(self): @@ -808,12 +839,15 @@ def examples(self, exclude_deleted=True): class RepositoryEntityQueryset(models.QuerySet): - def get(self, repository, value): + def get(self, repository, value, create_entity=True): try: return super().get( repository=repository, value=value) except self.model.DoesNotExist: + if not create_entity: + raise self.model.DoesNotExist + return super().create( repository=repository, value=value) @@ -862,10 +896,19 @@ class EntityBaseQueryset(models.QuerySet): def create(self, entity, **kwargs): if type(entity) is not RepositoryEntity: instance = self.model(**kwargs) - repository = instance.example.repository_update.repository + if 'repository_evaluate_id' in instance.__dict__: + evaluate = instance.repository_evaluate + repository = evaluate.repository_update.repository + elif 'evaluate_result_id' in instance.__dict__: + result = instance.evaluate_result + repository = result.repository_update.repository + else: + repository = instance.example.repository_update.repository + entity = RepositoryEntity.objects.get( repository=repository, value=entity) + return super().create( entity=entity, **kwargs) @@ -1200,6 +1243,210 @@ def send_request_approved_email(self): context)) +class RepositoryEvaluate(models.Model): + class Meta: + verbose_name = _('repository evaluate test') + verbose_name_plural = _('repository evaluate tests') + ordering = ['-created_at'] + db_table = 'common_repository_evaluate' + + repository_update = models.ForeignKey( + RepositoryUpdate, + models.CASCADE, + related_name='added_evaluate', + editable=False) + deleted_in = models.ForeignKey( + RepositoryUpdate, + models.CASCADE, + related_name='deleted_evaluate', + blank=True, + null=True) + text = models.TextField( + _('text'), + help_text=_('Evaluate test text')) + intent = models.CharField( + _('intent'), + max_length=64, + default='no_intent', + help_text=_('Evaluate intent reference'), + validators=[validate_item_key]) + created_at = models.DateTimeField( + _('created at'), + auto_now_add=True) + + @property + def language(self): + return self.repository_update.language + + def get_text(self, language=None): + if not language or language == self.repository_update.language: + return self.text + return None + + def get_entities(self, language): + if not language or language == self.repository_update.language: + return self.entities.all() + return None + + def delete(self): + self.deleted_in = self.repository_update.repository.current_update( + self.repository_update.language) + self.save(update_fields=['deleted_in']) + + def delete_entities(self): + self.entities.all().delete() + + +class RepositoryEvaluateEntity(EntityBase): + class Meta: + db_table = 'common_repository_evaluate_entity' + + repository_evaluate = models.ForeignKey( + RepositoryEvaluate, + models.CASCADE, + related_name='entities', + editable=False, + help_text=_('evaluate object')) + + def get_evaluate(self): + return self.repository_evaluate + + +class RepositoryEvaluateResultScore(models.Model): + class Meta: + db_table = 'common_repository_evaluate_result_score' + ordering = ['-created_at'] + + precision = models.DecimalField( + max_digits=3, + decimal_places=2, + null=True) + + f1_score = models.DecimalField( + max_digits=3, + decimal_places=2, + null=True) + + accuracy = models.DecimalField( + max_digits=3, + decimal_places=2, + null=True) + + recall = models.DecimalField( + max_digits=3, + decimal_places=2, + null=True) + + support = models.IntegerField( + null=True) + + created_at = models.DateTimeField( + _('created at'), + auto_now_add=True) + + +class RepositoryEvaluateResult(models.Model): + class Meta: + db_table = 'common_repository_evaluate_result' + verbose_name = _('evaluate results') + verbose_name_plural = _('evaluate results') + ordering = ['-created_at'] + + repository_update = models.ForeignKey( + RepositoryUpdate, + models.CASCADE, + editable=False, + related_name='results') + + intent_results = models.ForeignKey( + RepositoryEvaluateResultScore, + models.CASCADE, + editable=False, + related_name='intent_results') + + entity_results = models.ForeignKey( + RepositoryEvaluateResultScore, + models.CASCADE, + editable=False, + related_name='entity_results') + + matrix_chart = models.URLField( + verbose_name=_('Intent Confusion Matrix Chart'), + editable=False) + + confidence_chart = models.URLField( + verbose_name=_('Intent Prediction Confidence Distribution'), + editable=False) + + log = models.TextField( + verbose_name=_('Evaluate Log'), + blank=True, + editable=False) + + version = models.IntegerField( + verbose_name=_('Version'), + blank=False, + default=0, + editable=False) + + created_at = models.DateTimeField( + _('created at'), + auto_now_add=True) + + def save(self, *args, **kwargs): + repository = self.repository_update.repository + self.version = repository.evaluations_results().count() + 1 + return super().save(*args, **kwargs) + + +class RepositoryEvaluateResultIntent(models.Model): + class Meta: + db_table = 'common_repository_evaluate_result_intent' + + evaluate_result = models.ForeignKey( + RepositoryEvaluateResult, + models.CASCADE, + related_name='evaluate_result_intent' + ) + + intent = models.CharField( + _('intent'), + max_length=64, + help_text=_('Evaluate intent reference'), + validators=[validate_item_key]) + + score = models.ForeignKey( + RepositoryEvaluateResultScore, + models.CASCADE, + related_name='evaluation_intenties_score', + editable=False) + + +class RepositoryEvaluateResultEntity(models.Model): + class Meta: + db_table = 'common_repository_evaluate_result_entity' + + evaluate_result = models.ForeignKey( + RepositoryEvaluateResult, + models.CASCADE, + related_name='evaluate_result_entity' + ) + + entity = models.ForeignKey( + RepositoryEntity, + models.CASCADE, + related_name='entity', + editable=False) + + score = models.ForeignKey( + RepositoryEvaluateResultScore, + models.CASCADE, + related_name='evaluation_entities_score', + editable=False) + + objects = EntityBaseManager() + + @receiver(models.signals.pre_save, sender=RequestRepositoryAuthorization) def set_user_role_on_approved(instance, **kwargs): current = None diff --git a/bothub/health/checks.py b/bothub/health/checks.py index 7fb5a4a0..8e147b28 100644 --- a/bothub/health/checks.py +++ b/bothub/health/checks.py @@ -14,7 +14,7 @@ def check_database_connection(**kwargs): from django.db import connections from django.db.utils import OperationalError - if len(connections.all()) is 0: + if len(connections.all()) == 0: return False logger.info('found {} database connection'.format(len(connections.all()))) for i, conn in enumerate(connections.all(), 1):