diff --git a/.gitignore b/.gitignore index e17e6b7..5708994 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dmypy.json # Pyre type checker .pyre/ .idea +.vscode/ +data/ \ No newline at end of file diff --git a/Pipfile b/Pipfile index ccde694..07757c4 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,13 @@ verify_ssl = true [packages] requests = "*" pytest = "*" +assertpy = "*" +lxml = "*" +jsonpath-ng = "*" +cerberus = "*" +reportportal-client = "*" +pytest-reportportal = "*" +pytest-xdist = {extras = ["psutil"], version = "*"} [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index fc0f35a..fd3adbb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c8d9d4fb822dc60498ddcc1ea583873a3cdfd5094418db000376ddd01c15b487" + "sha256": "8a633053070ab315750f1c9515de3fe0e94f27fcb5008d75e8bccee551ffe091" }, "pipfile-spec": 6, "requires": { @@ -16,35 +16,82 @@ ] }, "default": { + "aenum": { + "hashes": [ + "sha256:07ea89f43d78b3d5997b32b8d5b0ec3e5be17b3e05b7bac0154b8c484a4aeff5", + "sha256:859fe994719e6b5e39f15f73acd84e08b4e57dc642373b177a5fa6646798706a", + "sha256:8dbe15f446eb8264b788dfeca163fb0a043d408d212152397dc11377b851e4ae" + ], + "version": "==3.1.8" + }, + "assertpy": { + "hashes": [ + "sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833" + ], + "index": "pypi", + "version": "==1.1" + }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.4.0" + }, + "cerberus": { + "hashes": [ + "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" + ], + "index": "pypi", + "version": "==1.3.4" }, "certifi": { "hashes": [ - "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", - "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "version": "==2020.11.8" + "markers": "python_version >= '3'", + "version": "==2.0.12" }, - "chardet": { + "decorator": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", + "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" ], - "version": "==3.0.4" + "markers": "python_version >= '3.5'", + "version": "==5.1.1" + }, + "dill": { + "hashes": [ + "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f", + "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675" + ], + "markers": "python_version >= '2.7' and python_version != '3.0'", + "version": "==0.3.4" + }, + "execnet": { + "hashes": [ + "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", + "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.9.0" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.3" }, "iniconfig": { "hashes": [ @@ -53,77 +100,240 @@ ], "version": "==1.1.1" }, + "jsonpath-ng": { + "hashes": [ + "sha256:292a93569d74029ba75ac2dc3d3630fc0e17b2df26119a165fa1d498ca47bf65", + "sha256:a273b182a82c1256daab86a313b937059261b5c5f8c4fa3fc38b882b344dd567", + "sha256:f75b95dbecb8a0f3b86fd2ead21c2b022c3f5770957492b9b6196ecccfeb10aa" + ], + "index": "pypi", + "version": "==1.5.3" + }, + "lxml": { + "hashes": [ + "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169", + "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428", + "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc", + "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85", + "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696", + "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507", + "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3", + "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430", + "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03", + "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9", + "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b", + "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7", + "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5", + "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654", + "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca", + "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9", + "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c", + "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63", + "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe", + "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9", + "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9", + "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1", + "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939", + "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68", + "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613", + "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63", + "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e", + "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4", + "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79", + "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1", + "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e", + "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141", + "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb", + "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939", + "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a", + "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93", + "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9", + "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2", + "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6", + "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa", + "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150", + "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea", + "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33", + "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76", + "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807", + "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a", + "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4", + "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15", + "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f", + "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429", + "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c", + "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5", + "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870", + "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b", + "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8", + "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c", + "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87", + "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0", + "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23", + "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170", + "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f" + ], + "index": "pypi", + "version": "==4.8.0" + }, "packaging": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4" + "markers": "python_version >= '3.6'", + "version": "==21.3" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "ply": { + "hashes": [ + "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", + "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce" + ], + "version": "==3.11" + }, + "psutil": { + "hashes": [ + "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5", + "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a", + "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4", + "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841", + "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d", + "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d", + "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0", + "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845", + "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf", + "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b", + "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07", + "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618", + "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2", + "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd", + "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666", + "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce", + "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3", + "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d", + "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25", + "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492", + "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b", + "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d", + "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2", + "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203", + "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2", + "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94", + "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9", + "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64", + "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56", + "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3", + "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c", + "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3" + ], + "version": "==5.9.0" }, "py": { "hashes": [ - "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", - "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.9.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", + "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.6'", + "version": "==3.0.7" }, "pytest": { "hashes": [ - "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe", - "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e" + "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", + "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" + ], + "index": "pypi", + "version": "==7.0.1" + }, + "pytest-forked": { + "hashes": [ + "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e", + "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8" + ], + "markers": "python_version >= '3.6'", + "version": "==1.4.0" + }, + "pytest-reportportal": { + "hashes": [ + "sha256:5b900d65047d59851e1dfb3a229275a92b5e6be030d880e847156d722a628ca7", + "sha256:9a55468d339e551955cc93247965c7e144f901da5949a8b847e0d965688154e4" + ], + "index": "pypi", + "version": "==5.0.12" + }, + "pytest-xdist": { + "extras": [ + "psutil" + ], + "hashes": [ + "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf", + "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65" ], "index": "pypi", - "version": "==6.1.2" + "version": "==2.5.0" + }, + "reportportal-client": { + "hashes": [ + "sha256:3910b3413311c47fdbed92b85fc01266e7a5185b406a35f8a6214eaa42f23a22", + "sha256:48bb05b7daf266f0eb5dbb8c7c5debcc257ece404f88a40c472ae61528b23f3c" + ], + "index": "pypi", + "version": "==5.1.0" }, "requests": { "hashes": [ - "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", - "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" ], "index": "pypi", - "version": "==2.25.0" + "version": "==2.27.1" + }, + "setuptools": { + "hashes": [ + "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", + "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" + ], + "markers": "python_version >= '3.7'", + "version": "==60.9.3" }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" + "version": "==1.16.0" }, - "toml": { + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" + "markers": "python_version >= '3.7'", + "version": "==2.0.1" }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.2" + "version": "==1.26.8" } }, "develop": {} diff --git a/README.md b/README.md index e89cd3c..6133800 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ -# course-api-framework-python -TAU course on Building an API framework with python +# Building an API test automation framework with Python + +## Purpose + +Code for TAU (Test automation university) course on building an API framework with Python. Once +ready this would be published at +[Test automation university](https://testautomationu.applitools.com/), You can also find a series of +blogs that I'm writing for this course on my blog +[https://automationhacks.io/](https://automationhacks.io/tags/) under `Python` tag. However, the +video courses are going to have much more context and in depth discussions + +## Setup + +Ensure you have +[pipenv already installed](https://automationhacks.io/2020/07/12/how-to-manage-your-python-virtualenvs-with-pipenv/): + +```zsh +# Activate virtualenv +pipenv shell +# Install all dependencies in your virtualenv +pipenv install +``` + +## How to navigate + +Each chapter has its own dedicated branch in `/example/_` format. For e.g. +`example/01_setup_python_dependencies` + +You can either use your IDE or terminal to switch to that branch and see the last updated commit. + +```zsh +# Checkout the entire branch +git checkout example/01_setup_python_dependencies +# Checkout to a specific commit, here can be found using `git log` command +git checkout +``` + +## Application under test + +This automated test suite covers features of `people-api`, Please refer the Github repo +[here](https://github.com/automationhacks/people-api). + +Note: These tests expect the `people-api` and `covid-tracker` API to be up. You would find +instructions in the `people-api` repo + +## How to run + +```zsh +# Setup report portal on docker +# Update rp_uuid in pytest.ini with project token +docker-compose -f docker-compose.yml -p reportportal up -d + +# Launch pipenv +pipenv shell + +# Install all packages +pipenv install + +# Run tests via pytest (single threaded) +python -m pytest + +# Run tests in parallel +python -m pytest -n auto + +# Report results to report portal +python -m pytest -n auto ./tests --reportportal +``` + +## Discuss? + +Feel free to use the +[Github discussions](https://github.com/automationhacks/course-api-framework-python/discussions/1) +in this repo to ✍🏼 your thoughts or even use the disqus comments section on the blogs. + +Happy learning! diff --git a/clients/__init__.py b/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clients/people/__init__.py b/clients/people/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clients/people/base_client.py b/clients/people/base_client.py new file mode 100644 index 0000000..1993a42 --- /dev/null +++ b/clients/people/base_client.py @@ -0,0 +1,6 @@ +class BaseClient: + def __init__(self): + self.headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } \ No newline at end of file diff --git a/clients/people/people_client.py b/clients/people/people_client.py new file mode 100644 index 0000000..992f429 --- /dev/null +++ b/clients/people/people_client.py @@ -0,0 +1,45 @@ +from json import dumps +from uuid import uuid4 + +from clients.people.base_client import BaseClient +from config import BASE_URI +from utils.request import APIRequest + + +class PeopleClient(BaseClient): + def __init__(self): + super().__init__() + + self.base_url = BASE_URI + self.request = APIRequest() + + def create_person(self, body=None): + last_name, response = self.__create_person_with_unique_last_name(body) + return last_name, response + + def __create_person_with_unique_last_name(self, body=None): + if body is None: + last_name = f'User {str(uuid4())}' + payload = dumps({ + 'fname': 'New', + 'lname': last_name + }) + else: + last_name = body['lname'] + payload = dumps(body) + + response = self.request.post(self.base_url, payload, self.headers) + return last_name, response + + def read_one_person_by_id(self, person_id): + pass + + def read_all_persons(self): + return self.request.get(self.base_url) + + def update_person(self): + pass + + def delete_person(self, person_id): + url = f'{BASE_URI}/{person_id}' + return self.request.delete(url) diff --git a/config.py b/config.py new file mode 100644 index 0000000..2279649 --- /dev/null +++ b/config.py @@ -0,0 +1,2 @@ +BASE_URI = 'http://0.0.0.0:5000/api/people' +COVID_TRACKER_HOST = 'http://127.0.0.1:3000' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e4748fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,248 @@ +## You can generate a custom docker compose file automatically on http://reportportal.io/download (Step 2) + +## This is example of Docker Compose for ReportPortal +## Do not forget to configure data volumes for production usage + +## Execute 'docker-compose -f docker-compose.yml -p reportportal up -d --force-recreate' --build +## to start all containers in daemon mode +## Where: +## '-f docker-compose.yml' -- specifies this compose file +## '-p reportportal' -- specifies container's prefix (project name) +## '-d' -- enables daemon mode +## '--force-recreate' -- forces re-recreating of all containers + +version: '2.4' +services: + + gateway: + image: traefik:v2.0.5 + ports: + - "8080:8080" # HTTP exposed + - "8081:8081" # HTTP Administration exposed + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: + - --providers.docker=true + - --providers.docker.constraints=Label(`traefik.expose`, `true`) + - --entrypoints.web.address=:8080 + - --entrypoints.traefik.address=:8081 + - --api.dashboard=true + - --api.insecure=true + restart: always + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.3.0 + volumes: + - ./data/elasticsearch:/usr/share/elasticsearch/data + environment: + - "bootstrap.memory_lock=true" + - "discovery.type=single-node" + - "logger.level=INFO" + - "xpack.security.enabled=true" + - "ELASTIC_PASSWORD=elastic1q2w3e" + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + # ports: + # - "9200:9200" + healthcheck: + test: ["CMD", "curl","-s" ,"-f", "http://elastic:elastic1q2w3e@localhost:9200/_cat/health"] + restart: always + + analyzer: + image: reportportal/service-auto-analyzer:5.3.4 + environment: + LOGGING_LEVEL: info + AMQP_URL: amqp://rabbitmq:rabbitmq@rabbitmq:5672 + ES_HOSTS: http://elastic:elastic1q2w3e@elasticsearch:9200 + MINIO_SHORT_HOST: minio:9000 + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + depends_on: + - elasticsearch + restart: always + + ### Initial reportportal db schema. Run once. + db-scripts: + image: reportportal/migrations:5.3.4 + depends_on: + postgres: + condition: service_healthy + environment: + POSTGRES_SERVER: postgres + POSTGRES_PORT: 5432 + POSTGRES_DB: reportportal + POSTGRES_USER: rpuser + POSTGRES_PASSWORD: rppass + restart: on-failure + + postgres: + image: postgres:12-alpine + shm_size: '512m' + environment: + POSTGRES_USER: rpuser + POSTGRES_PASSWORD: rppass + POSTGRES_DB: reportportal + volumes: + # For unix host + - ./data/postgres:/var/lib/postgresql/data + # For windows host + # - postgres:/var/lib/postgresql/data + # If you need to access the DB locally. Could be a security risk to expose DB. + # ports: + # - "5432:5432" + command: + -c checkpoint_completion_target=0.9 + -c work_mem=96MB + -c wal_writer_delay=20ms + -c synchronous_commit=off + -c wal_buffers=32MB + -c min_wal_size=2GB + -c max_wal_size=4GB + # Optional, for SSD Data Storage. If you are using the HDD, set up this command to '2' + # -c effective_io_concurrency=200 + # Optional, for SSD Data Storage. If you are using the HDD, set up this command to '4' + # -c random_page_cost=1.1 + # Optional, can be scaled. Example for 4 CPU, 16GB RAM instance, where only the database is deployed + # -c max_worker_processes=4 + # -c max_parallel_workers_per_gather=2 + # -c max_parallel_workers=4 + # -c shared_buffers=4GB + # -c effective_cache_size=12GB + # -c maintenance_work_mem=1GB + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $$POSTGRES_DB -U $$POSTGRES_USER"] + interval: 10s + timeout: 120s + retries: 10 + restart: always + + rabbitmq: + image: rabbitmq:3.7.16-management + #ports: + # - "5672:5672" + #- "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: "rabbitmq" + RABBITMQ_DEFAULT_PASS: "rabbitmq" + healthcheck: + test: ["CMD", "rabbitmqctl", "status"] + retries: 5 + restart: always + + uat: + image: reportportal/service-authorization:5.3.3 + #ports: + # - "9999:9999" + environment: + - RP_DB_HOST=postgres + - RP_DB_USER=rpuser + - RP_DB_PASS=rppass + - RP_DB_NAME=reportportal + - RP_BINARYSTORE_TYPE=minio + - RP_BINARYSTORE_MINIO_ENDPOINT=http://minio:9000 + - RP_BINARYSTORE_MINIO_ACCESSKEY=minio + - RP_BINARYSTORE_MINIO_SECRETKEY=minio123 + - RP_SESSION_LIVE=86400 #in seconds + labels: + - "traefik.http.middlewares.uat-strip-prefix.stripprefix.prefixes=/uat" + - "traefik.http.routers.uat.middlewares=uat-strip-prefix@docker" + - "traefik.http.routers.uat.rule=PathPrefix(`/uat`)" + - "traefik.http.routers.uat.service=uat" + - "traefik.http.services.uat.loadbalancer.server.port=9999" + - "traefik.http.services.uat.loadbalancer.server.scheme=http" + - "traefik.expose=true" + restart: always + + index: + image: reportportal/service-index:5.0.10 + depends_on: + gateway: + condition: service_started + environment: + - LB_URL=http://gateway:8081 + - TRAEFIK_V2_MODE=true + labels: + - "traefik.http.routers.index.rule=PathPrefix(`/`)" + - "traefik.http.routers.index.service=index" + - "traefik.http.services.index.loadbalancer.server.port=8080" + - "traefik.http.services.index.loadbalancer.server.scheme=http" + - "traefik.expose=true" + restart: always + + api: + image: reportportal/service-api:5.3.4 + depends_on: + rabbitmq: + condition: service_healthy + gateway: + condition: service_started + postgres: + condition: service_healthy + environment: + - RP_DB_HOST=postgres + - RP_DB_USER=rpuser + - RP_DB_PASS=rppass + - RP_DB_NAME=reportportal + - RP_AMQP_USER=rabbitmq + - RP_AMQP_PASS=rabbitmq + - RP_AMQP_APIUSER=rabbitmq + - RP_AMQP_APIPASS=rabbitmq + - RP_BINARYSTORE_TYPE=minio + - RP_BINARYSTORE_MINIO_ENDPOINT=http://minio:9000 + - RP_BINARYSTORE_MINIO_ACCESSKEY=minio + - RP_BINARYSTORE_MINIO_SECRETKEY=minio123 + - LOGGING_LEVEL_ORG_HIBERNATE_SQL=info + - JAVA_OPTS=-Xmx1g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Dcom.sun.management.jmxremote.rmi.port=12349 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 + labels: + - "traefik.http.middlewares.api-strip-prefix.stripprefix.prefixes=/api" + - "traefik.http.routers.api.middlewares=api-strip-prefix@docker" + - "traefik.http.routers.api.rule=PathPrefix(`/api`)" + - "traefik.http.routers.api.service=api" + - "traefik.http.services.api.loadbalancer.server.port=8585" + - "traefik.http.services.api.loadbalancer.server.scheme=http" + - "traefik.expose=true" + restart: always + + ui: + image: reportportal/service-ui:5.3.4 + environment: + - RP_SERVER_PORT=8080 + labels: + - "traefik.http.middlewares.ui-strip-prefix.stripprefix.prefixes=/ui" + - "traefik.http.routers.ui.middlewares=ui-strip-prefix@docker" + - "traefik.http.routers.ui.rule=PathPrefix(`/ui`)" + - "traefik.http.routers.ui.service=ui" + - "traefik.http.services.ui.loadbalancer.server.port=8080" + - "traefik.http.services.ui.loadbalancer.server.scheme=http" + - "traefik.expose=true" + restart: always + + minio: + image: minio/minio:RELEASE.2020-10-27T04-03-55Z + #ports: + # - '9000:9000' + volumes: + # For unix host + - ./data/storage:/data + # For windows host + # - minio:/data + environment: + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + command: server /data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + restart: always + + # Docker volume for Windows host +#volumes: +# postgres: +# minio: diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5ec92d9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +rp_uuid = 3a105970-9304-4c08-93c0-fc6b063a27ae +rp_endpoint = http://localhost:8080 +rp_project = people-tests +rp_launch = people-tests +# Additional tags that would be printed +rp_launch_attributes = 'people-api' 'covid-tracker' +# Would be mentioned as a description of any test suite run +rp_launch_description = 'People and covid tracker tests' +rp_ignore_errors = True +rp_ignore_attributes = 'xfail' 'usefixture' \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/assertions/__init__.py b/tests/assertions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/assertions/people_assertions.py b/tests/assertions/people_assertions.py new file mode 100644 index 0000000..4bfc4ee --- /dev/null +++ b/tests/assertions/people_assertions.py @@ -0,0 +1,9 @@ +from assertpy import assert_that + + +def assert_people_have_person_with_first_name(response, first_name): + assert_that(response.as_dict).extracting('fname').is_not_empty().contains(first_name) + + +def assert_person_is_present(is_new_user_created): + assert_that(is_new_user_created).is_not_empty() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8a17b36 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,43 @@ +import logging +import random +import sys + +import pytest +from pytest_reportportal import RPLogger, RPLogHandler + +from utils.file_reader import read_file + + +@pytest.fixture +def create_data(): + payload = read_file('create_person.json') + + random_no = random.randint(0, 1000) + last_name = f'Olabini{random_no}' + + payload['lname'] = last_name + yield payload + + +@pytest.fixture(scope="session") +def logger(request): + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + + # Create handler for Report Portal if the service has been + # configured and started. + if hasattr(request.node.config, 'py_test_service'): + # Import Report Portal logger and handler to the test module. + logging.setLoggerClass(RPLogger) + rp_handler = RPLogHandler(request.node.config.py_test_service) + + # Add additional handlers if it is necessary + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.INFO) + logger.addHandler(console_handler) + else: + rp_handler = logging.StreamHandler(sys.stdout) + + # Set INFO level for Report Portal handler. + rp_handler.setLevel(logging.INFO) + return logger diff --git a/tests/covid_test.py b/tests/covid_test.py new file mode 100644 index 0000000..07e3c0b --- /dev/null +++ b/tests/covid_test.py @@ -0,0 +1,37 @@ +import requests +from assertpy import assert_that +from lxml import etree + +from config import COVID_TRACKER_HOST +from utils.print_helpers import pretty_print + + +def test_covid_cases_have_crossed_a_million(): + response = requests.get(f'{COVID_TRACKER_HOST}/api/v1/summary/latest') + pretty_print(response.headers) + + response_xml = response.text + xml_tree = etree.fromstring(bytes(response_xml, encoding='utf8')) + + # use .xpath on xml_tree object to evaluate the expression + total_cases = xml_tree.xpath("//data/summary/total_cases")[0].text + assert_that(int(total_cases)).is_greater_than(1000000) + + +def test_overall_covid_cases_match_sum_of_total_cases_by_country(): + response = requests.get(f'{COVID_TRACKER_HOST}/api/v1/summary/latest') + pretty_print(response.headers) + + response_xml = response.text + xml_tree = etree.fromstring(bytes(response_xml, encoding='utf8')) + + overall_cases = int(xml_tree.xpath("//data/summary/total_cases")[0].text) + # Another way to specify XPath first and then use to evaluate + # on an XML tree + search_for = etree.XPath("//data//regions//total_cases") + cases_by_country = 0 + for region in search_for(xml_tree): + cases_by_country += int(region.text) + + assert_that(overall_cases).is_greater_than(cases_by_country) + diff --git a/tests/data/create_person.json b/tests/data/create_person.json new file mode 100644 index 0000000..b909072 --- /dev/null +++ b/tests/data/create_person.json @@ -0,0 +1,4 @@ +{ + "fname": "Sample firstname", + "lname": "Sample lastname" +} \ No newline at end of file diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/helpers/people_helpers.py b/tests/helpers/people_helpers.py new file mode 100644 index 0000000..a0d9fa8 --- /dev/null +++ b/tests/helpers/people_helpers.py @@ -0,0 +1,10 @@ +from jsonpath_ng import parse + + +def search_created_user_in(peoples, last_name): + return [person for person in peoples if person['lname'] == last_name][0] + + +def search_nodes_using_json_path(peoples, json_path): + jsonpath_expr = parse(json_path) + return [match.value for match in jsonpath_expr.find(peoples)] diff --git a/tests/people_test.py b/tests/people_test.py new file mode 100644 index 0000000..716e63a --- /dev/null +++ b/tests/people_test.py @@ -0,0 +1,49 @@ +import requests + +from clients.people.people_client import PeopleClient +from tests.assertions.people_assertions import * +from tests.helpers.people_helpers import * + +client = PeopleClient() + + +def test_read_all_has_kent(logger): + """ + Test on hitting People GET API, we get a user named kent in the list of people + """ + response = client.read_all_persons() + + assert_that(response.status_code).is_equal_to(requests.codes.ok) + logger.info("User successfully read") + assert_people_have_person_with_first_name(response, first_name='Kent') + + +def test_new_person_can_be_added(): + last_name, response = client.create_person() + assert_that(response.status_code, description='Person not created').is_equal_to(requests.codes.no_content) + + peoples = client.read_all_persons().as_dict + is_new_user_created = search_created_user_in(peoples, last_name) + assert_person_is_present(is_new_user_created) + + +def test_created_person_can_be_deleted(): + persons_last_name, _ = client.create_person() + + peoples = client.read_all_persons().as_dict + new_person_id = search_created_user_in(peoples, persons_last_name)['person_id'] + + response = client.delete_person(new_person_id) + assert_that(response.status_code).is_equal_to(requests.codes.ok) + + +def test_person_can_be_added_with_a_json_template(create_data): + client.create_person(create_data) + + response = client.read_all_persons() + peoples = response.as_dict + + result = search_nodes_using_json_path(peoples, json_path="$.[*].lname") + + expected_last_name = create_data['lname'] + assert_that(result).contains(expected_last_name) diff --git a/tests/schema_test.py b/tests/schema_test.py new file mode 100644 index 0000000..ada361e --- /dev/null +++ b/tests/schema_test.py @@ -0,0 +1,36 @@ +import json + +import requests +from assertpy import assert_that, soft_assertions +from cerberus import Validator + +from config import BASE_URI + +schema = { + "fname": {'type': 'string'}, + "lname": {'type': 'string'}, + "person_id": {'type': 'number'}, + "timestamp": {'type': 'string'} +} + + +def test_read_one_operation_has_expected_schema(): + response = requests.get(f'{BASE_URI}/1') + person = json.loads(response.text) + + validator = Validator(schema, require_all=True) + is_valid = validator.validate(person) + + assert_that(is_valid, description=validator.errors).is_true() + + +def test_read_all_operation_has_expected_schema(): + response = requests.get(BASE_URI) + persons = json.loads(response.text) + + validator = Validator(schema, require_all=True) + + with soft_assertions(): + for person in persons: + is_valid = validator.validate(person) + assert_that(is_valid, description=validator.errors).is_true() diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/file_reader.py b/utils/file_reader.py new file mode 100644 index 0000000..6b0c496 --- /dev/null +++ b/utils/file_reader.py @@ -0,0 +1,19 @@ +import json +from pathlib import Path + +BASE_PATH = Path.cwd().joinpath('tests', 'data') + + +def read_file(file_name): + path = get_file_with_json_extension(file_name) + + with path.open(mode='r') as f: + return json.load(f) + + +def get_file_with_json_extension(file_name): + if '.json' in file_name: + path = BASE_PATH.joinpath(file_name) + else: + path = BASE_PATH.joinpath(f'{file_name}.json') + return path diff --git a/utils/print_helpers.py b/utils/print_helpers.py new file mode 100644 index 0000000..dfd81cc --- /dev/null +++ b/utils/print_helpers.py @@ -0,0 +1,6 @@ +from pprint import pprint + + +def pretty_print(msg, indent=2): + print() + pprint(msg, indent=indent) \ No newline at end of file diff --git a/utils/request.py b/utils/request.py new file mode 100644 index 0000000..22cfd98 --- /dev/null +++ b/utils/request.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass + +import requests + + +@dataclass +class Response: + status_code: int + text: str + as_dict: object + headers: dict + + +class APIRequest: + def get(self, url): + response = requests.get(url) + return self.__get_responses(response) + + def post(self, url, payload, headers): + response = requests.post(url, data=payload, headers=headers) + return self.__get_responses(response) + + def delete(self, url): + response = requests.delete(url) + return self.__get_responses(response) + + def __get_responses(self, response): + status_code = response.status_code + text = response.text + + try: + as_dict = response.json() + except Exception: + as_dict = {} + + headers = response.headers + + return Response( + status_code, text, as_dict, headers + )