diff --git a/Pipfile.lock b/Pipfile.lock index 12f96c2..607b63b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -544,11 +544,11 @@ }, "flask": { "hashes": [ - "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", - "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d" + "sha256:13f6329ddbfff11340939cd11919daf150a01358ded4b7e81c03c055dfecb559", + "sha256:77504c4c097f56ac5f29b00f9009213010cf9d2923a288c0e0564a5db2bb53d6" ], "markers": "python_version >= '3.7'", - "version": "==2.2.3" + "version": "==2.2.4" }, "flask-appbuilder": { "hashes": [ @@ -1377,11 +1377,11 @@ }, "requests": { "hashes": [ - "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", - "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b", + "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059" ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.2" + "markers": "python_version >= '3.7'", + "version": "==2.29.0" }, "requests-toolbelt": { "hashes": [ @@ -1401,11 +1401,11 @@ }, "rich": { "hashes": [ - "sha256:22b74cae0278fd5086ff44144d3813be1cedc9115bdfabbfefd86400cb88b20a", - "sha256:b5d573e13605423ec80bdd0cd5f8541f7844a0e71a13f74cf454ccb2f490708b" + "sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c", + "sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.3.4" + "version": "==13.3.5" }, "setproctitle": { "hashes": [ @@ -1573,11 +1573,11 @@ }, "sqlalchemy-utils": { "hashes": [ - "sha256:894cce255eea0bcc4fdcff628af30219d24a325526011586dd7f1e3d9dfebba0", - "sha256:986b4140f7740ff37244f6ed9182e8c997caa334150773de5932009b2490fb50" + "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801", + "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74" ], "markers": "python_version >= '3.6'", - "version": "==0.41.0" + "version": "==0.41.1" }, "sqlparse": { "hashes": [ @@ -1747,83 +1747,83 @@ }, "yarl": { "hashes": [ - "sha256:01a073c9175481dfed6b40704a1b67af5a9435fc4a58a27d35fd6b303469b0c7", - "sha256:01cf88cb80411978a14aa49980968c1aeb7c18a90ac978c778250dd234d8e0ba", - "sha256:08c8599d6aa8a24425f8635f6c06fa8726afe3be01c8e53e236f519bcfa5db5b", - "sha256:098bdc06ffb4db39c73883325b8c738610199f5f12e85339afedf07e912a39af", - "sha256:09c56a32c26e24ef98d5757c5064e252836f621f9a8b42737773aa92936b8e08", - "sha256:13a1ad1f35839b3bb5226f59816b71e243d95d623f5b392efaf8820ddb2b3cd5", - "sha256:1baf8cdaaab65d9ccedbf8748d626ad648b74b0a4d033e356a2f3024709fb82f", - "sha256:1d7a0075a55380b19aa43b9e8056e128b058460d71d75018a4f9d60ace01e78c", - "sha256:27efc2e324f72df02818cd72d7674b1f28b80ab49f33a94f37c6473c8166ce49", - "sha256:307a782736ebf994e7600dcaeea3b3113083584da567272f2075f1540919d6b3", - "sha256:395ea180257a3742d09dcc5071739682a95f7874270ebe3982d6696caec75be0", - "sha256:39a7a9108e9fc633ae381562f8f0355bb4ba00355218b5fb19cf5263fcdbfa68", - "sha256:3abe37fd89a93ebe0010417ca671f422fa6fcffec54698f623b09f46b4d4a512", - "sha256:4295790981630c4dab9d6de7b0f555a4c8defe3ed7704a8e9e595a321e59a0f5", - "sha256:44fa6158e6b4b8ccfa2872c3900a226b29e8ce543ce3e48aadc99816afa8874d", - "sha256:46c4010de941e2e1365c07fb4418ddca10fcff56305a6067f5ae857f8c98f3a7", - "sha256:4764114e261fe49d5df9b316b3221493d177247825c735b2aae77bc2e340d800", - "sha256:4d817593d345fefda2fae877accc8a0d9f47ada57086da6125fa02a62f6d1a94", - "sha256:518a92a34c741836a315150460b5c1c71ae782d569eabd7acf53372e437709f7", - "sha256:56956b13ec275de31fe4fb991510b735c4fb3e1b01600528c952b9ac90464430", - "sha256:575975d28795a61e82c85f114c02333ca54cbd325fd4e4b27598c9832aa732e7", - "sha256:5ce0bcab7ec759062c818d73837644cde567ab8aa1e0d6c45db38dfb7c284441", - "sha256:5faf3ec98747318cb980aaf9addf769da68a66431fc203a373d95d7ee9c1fbb4", - "sha256:65d952e464df950eed32bb5dcbc1b4443c7c2de4d7abd7265b45b1b3b27f5fa2", - "sha256:6b09cce412386ea9b4dda965d8e78d04ac5b5792b2fa9cced3258ec69c7d1c16", - "sha256:6cdb47cbbacae8e1d7941b0d504d0235d686090eef5212ca2450525905e9cf02", - "sha256:6cf47fe9df9b1ededc77e492581cdb6890a975ad96b4172e1834f1b8ba0fc3ba", - "sha256:73a4b46689f2d59c8ec6b71c9a0cdced4e7863dd6eb98a8c30ea610e191f9e1c", - "sha256:74390c2318d066962500045aa145f5412169bce842e734b8c3e6e3750ad5b817", - "sha256:75676110bce59944dd48fd18d0449bd37eaeb311b38a0c768f7670864b5f8b68", - "sha256:78755ce43b6e827e65ec0c68be832f86d059fcf05d4b33562745ebcfa91b26b1", - "sha256:791357d537a09a194f92b834f28c98d074e7297bac0a8f1d5b458a906cafa17c", - "sha256:85aa6fd779e194901386709e0eedd45710b68af2709f82a84839c44314b68c10", - "sha256:88f6413ff5edfb9609e2769e32ce87a62353e66e75d264bf0eaad26fb9daa8f2", - "sha256:89099c887338608da935ba8bee027564a94f852ac40e472de15d8309517ad5fe", - "sha256:89da1fd6068553e3a333011cc17ad91c414b2100c32579ddb51517edc768b49c", - "sha256:8c72a1dc7e2ea882cd3df0417c808ad3b69e559acdc43f3b096d67f2fb801ada", - "sha256:90ebaf448b5f048352ec7c76cb8d452df30c27cb6b8627dfaa9cf742a14f141a", - "sha256:92a101f6d5a9464e86092adc36cd40ef23d18a25bfb1eb32eaeb62edc22776bb", - "sha256:92e37999e36f9f3ded78e9d839face6baa2abdf9344ea8ed2735f495736159de", - "sha256:97d76a3128f48fa1c721ef8a50e2c2f549296b2402dc8a8cde12ff60ed922f53", - "sha256:9ba5a18c4fbd408fe49dc5da85478a76bc75c1ce912d7fd7b43ed5297c4403e1", - "sha256:9bb794882818fae20ff65348985fdf143ea6dfaf6413814db1848120db8be33e", - "sha256:a21789bdf28549d4eb1de6910cabc762c9f6ae3eef85efc1958197c1c6ef853b", - "sha256:a8b8d4b478a9862447daef4cafc89d87ea4ed958672f1d11db7732b77ead49cc", - "sha256:ac8e593df1fbea820da7676929f821a0c7c2cecb8477d010254ce8ed54328ea8", - "sha256:b20a5ddc4e243cbaa54886bfe9af6ffc4ba4ef58f17f1bb691e973eb65bba84d", - "sha256:b2b2382d59dec0f1fdca18ea429c4c4cee280d5e0dbc841180abb82e188cf6e9", - "sha256:b3b5f8da07a21f2e57551f88a6709c2d340866146cf7351e5207623cfe8aad16", - "sha256:b5d5fb6c94b620a7066a3adb7c246c87970f453813979818e4707ac32ce4d7bd", - "sha256:b63d41e0eecf3e3070d44f97456cf351fff7cb960e97ecb60a936b877ff0b4f6", - "sha256:b86e98c3021b7e2740d8719bf074301361bf2f51221ca2765b7a58afbfbd9042", - "sha256:bab67d041c78e305ff3eef5e549304d843bd9b603c8855b68484ee663374ce15", - "sha256:c3ca8d71b23bdf164b36d06df2298ec8a5bd3de42b17bf3e0e8e6a7489195f2c", - "sha256:ca14b84091700ae7c1fcd3a6000bd4ec1a3035009b8bcb94f246741ca840bb22", - "sha256:d21887cbcf6a3cc5951662d8222bc9c04e1b1d98eebe3bb659c3a04ed49b0eec", - "sha256:d5c407e530cf2979ea383885516ae79cc4f3c3530623acf5e42daf521f5c2564", - "sha256:d966cd59df9a4b218480562e8daab39e87e746b78a96add51a3ab01636fc4291", - "sha256:df747104ef27ab1aa9a1145064fa9ea26ad8cf24bfcbdba7db7abf0f8b3676b9", - "sha256:e124b283a04cc06d22443cae536f93d86cd55108fa369f22b8fe1f2288b2fe1c", - "sha256:e2f01351b7809182822b21061d2a4728b7b9e08f4585ba90ee4c5c4d3faa0812", - "sha256:e7ddebeabf384099814353a2956ed3ab5dbaa6830cc7005f985fcb03b5338f05", - "sha256:e9fe3a1c073ab80a28a06f41d2b623723046709ed29faf2c56bea41848597d86", - "sha256:ecaa5755a39f6f26079bf13f336c67af589c222d76b53cd3824d3b684b84d1f1", - "sha256:ecad20c3ef57c513dce22f58256361d10550a89e8eaa81d5082f36f8af305375", - "sha256:eed9827033b7f67ad12cb70bd0cb59d36029144a7906694317c2dbf5c9eb5ddd", - "sha256:ef7e2f6c47c41e234600a02e1356b799761485834fe35d4706b0094cb3a587ee", - "sha256:efec77851231410125cb5be04ec96fa4a075ca637f415a1f2d2c900b09032a8a", - "sha256:f0cd87949d619157a0482c6c14e5011f8bf2bc0b91cb5087414d9331f4ef02dd", - "sha256:f206adb89424dca4a4d0b31981869700e44cd62742527e26d6b15a510dd410a2", - "sha256:f5bcb80006efe9bf9f49ae89711253dd06df8053ff814622112a9219346566a7", - "sha256:f76edb386178a54ea7ceffa798cb830c3c22ab50ea10dfb25dc952b04848295f", - "sha256:f878a78ed2ccfbd973cab46dd0933ecd704787724db23979e5731674d76eb36f", - "sha256:f8e73f526140c1c32f5fca4cd0bc3b511a1abcd948f45b2a38a95e4edb76ca72" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.1" + "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571", + "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3", + "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3", + "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c", + "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7", + "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04", + "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191", + "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea", + "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4", + "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4", + "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095", + "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e", + "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74", + "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef", + "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33", + "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde", + "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45", + "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf", + "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b", + "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac", + "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0", + "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528", + "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716", + "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb", + "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18", + "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72", + "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6", + "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582", + "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5", + "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368", + "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc", + "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9", + "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be", + "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a", + "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80", + "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8", + "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6", + "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417", + "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574", + "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59", + "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608", + "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82", + "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1", + "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3", + "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d", + "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8", + "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc", + "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac", + "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8", + "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955", + "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0", + "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367", + "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb", + "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a", + "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623", + "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2", + "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6", + "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7", + "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4", + "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051", + "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938", + "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8", + "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9", + "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3", + "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5", + "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9", + "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333", + "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185", + "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3", + "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560", + "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b", + "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7", + "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78", + "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.2" } }, "develop": { @@ -2425,60 +2425,60 @@ "toml" ], "hashes": [ - "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93", - "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013", - "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f", - "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21", - "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462", - "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc", - "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df", - "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1", - "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235", - "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934", - "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9", - "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1", - "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48", - "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4", - "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe", - "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a", - "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b", - "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21", - "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d", - "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa", - "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367", - "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535", - "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152", - "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e", - "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539", - "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1", - "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925", - "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0", - "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2", - "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab", - "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841", - "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30", - "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91", - "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c", - "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257", - "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9", - "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040", - "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911", - "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623", - "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259", - "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c", - "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79", - "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5", - "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4", - "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4", - "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22", - "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd", - "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1", - "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910", - "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859", - "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312" - ], - "markers": "python_version >= '3.7'", - "version": "==7.2.3" + "sha256:00f8fd8a5fe1ffc3aef78ea2dbf553e5c0f4664324e878995e38d41f037eb2b3", + "sha256:0b65a6a5484b7f2970393d6250553c05b2ede069e0e18abe907fdc7f3528252e", + "sha256:12bc9127c8aca2f7c25c9acca53da3db6799b2999b40f28c2546237b7ea28459", + "sha256:1a3e8697cb40f28e5bcfb6f4bda7852d96dbb6f6fd7cc306aba4ae690c9905ab", + "sha256:1d2a9180beff1922b09bd7389e23454928e108449e646c26da5c62e29b0bf4e3", + "sha256:1d3893f285fd76f56651f04d1efd3bdce251c32992a64c51e5d6ec3ba9e3f9c9", + "sha256:2857894c22833d3da6e113623a9b7440159b2295280b4e0d954cadbfa724b85a", + "sha256:29c7d88468f01a75231797173b52dc66d20a8d91b8bb75c88fc5861268578f52", + "sha256:2d784177a7fb9d0f58d24d3e60638c8b729c3693963bf67fa919120f750db237", + "sha256:39747afc854a7ee14e5e132da7db179d6281faf97dc51e6d7806651811c47538", + "sha256:3d6f3c5b6738a494f17c73b4aa3aa899865cc33a74aa85e3b5695943b79ad3ce", + "sha256:3fc9cde48de956bfbacea026936fbd4974ff1dc2f83397c6f1968f0142c9d50b", + "sha256:4078939c4b7053e14e87c65aa68dbed7867e326e450f94038bfe1a1b22078ff9", + "sha256:437da7d2fcc35bf45e04b7e9cfecb7c459ec6f6dc17a8558ed52e8d666c2d9ab", + "sha256:4522dd9aeb9cc2c4c54ce23933beb37a4e106ec2ba94f69138c159024c8a906a", + "sha256:50fda3d33b705b9c01e3b772cfa7d14de8aec2ec2870e4320992c26d057fde12", + "sha256:56a674ad18d6b04008283ca03c012be913bf89d91c0803c54c24600b300d9e51", + "sha256:56d74d6fbd5a98a5629e8467b719b0abea9ca01a6b13555d125c84f8bf4ea23d", + "sha256:5c122d120c11a236558c339a59b4b60947b38ac9e3ad30a0e0e02540b37bf536", + "sha256:5c6c6e3b8fb6411a2035da78d86516bfcfd450571d167304911814407697fb7a", + "sha256:603a2b172126e3b08c11ca34200143089a088cd0297d4cfc4922d2c1c3a892f9", + "sha256:60feb703abc8d78e9427d873bcf924c9e30cf540a21971ef5a17154da763b60f", + "sha256:6a17bf32e9e3333d78606ac1073dd20655dc0752d5b923fa76afd3bc91674ab4", + "sha256:700bc9fb1074e0c67c09fe96a803de66663830420781df8dc9fb90d7421d4ccb", + "sha256:72751d117ceaad3b1ea3bcb9e85f5409bbe9fb8a40086e17333b994dbccc0718", + "sha256:7283f78d07a201ac7d9dc2ac2e4faaea99c4d302f243ee5b4e359f3e170dc008", + "sha256:856bcb837e96adede31018a0854ce7711a5d6174db1a84e629134970676c54fa", + "sha256:864e36947289be05abd83267c4bade35e772526d3e9653444a9dc891faf0d698", + "sha256:8769a67e8816c7e94d5bf446fc0501641fde78fdff362feb28c2c64d45d0e9b1", + "sha256:876e4ef3eff00b50787867c5bae84857a9af4c369a9d5b266cd9b19f61e48ef7", + "sha256:89e63b38c7b888e00fd42ce458f838dccb66de06baea2da71801b0fc9070bfa0", + "sha256:92b565c51732ea2e7e541709ccce76391b39f4254260e5922e08e00971e88e33", + "sha256:9e5eedde6e6e241ec3816f05767cc77e7456bf5ec6b373fb29917f0990e2078f", + "sha256:a5c4f2e44a2ae15fa6883898e756552db5105ca4bd918634cbd5b7c00e19e8a1", + "sha256:ab08af91cf4d847a6e15d7d5eeae5fead1487caf16ff3a2056dbe64d058fd246", + "sha256:ab08e03add2cf5793e66ac1bbbb24acfa90c125476f5724f5d44c56eeec1d635", + "sha256:ac4861241e693e21b280f07844ae0e0707665e1dfcbf9466b793584984ae45c4", + "sha256:b3023ce23e41a6f006c09f7e6d62b6c069c36bdc9f7de16a5ef823acc02e6c63", + "sha256:bc47015fc0455753e8aba1f38b81b731aaf7f004a0c390b404e0fcf1d6c1d72f", + "sha256:c2becddfcbf3d994a8f4f9dd2b6015cae3a3eff50dedc6e4a17c3cccbe8f93d4", + "sha256:cdee9a77fd0ce000781680b6a1f4b721c567f66f2f73a49be1843ff439d634f3", + "sha256:cdfb53bef4b2739ff747ebbd76d6ac5384371fd3c7a8af08899074eba034d483", + "sha256:d4db4e6c115d869cd5397d3d21fd99e4c7053205c33a4ae725c90d19dcd178af", + "sha256:d9f770c6052d9b5c9b0e824fd8c003fe33276473b65b4f10ece9565ceb62438e", + "sha256:e41a7f44e73b37c6f0132ecfdc1c8b67722f42a3d9b979e6ebc150c8e80cf13a", + "sha256:ea534200efbf600e60130c48552f99f351cae2906898a9cd924c1c7f2fb02853", + "sha256:f19ba9301e6fb0b94ba71fda9a1b02d11f0aab7f8e2455122a4e2921b6703c2f", + "sha256:f37ae1804596f13d811e0247ffc8219f5261b3565bdf45fcbb4fc091b8e9ff35", + "sha256:f7668a621afc52db29f6867e0e9c72a1eec9f02c94a7c36599119d557cf6e471", + "sha256:f7ffdb3af2a01ce91577f84fc0faa056029fe457f3183007cffe7b11ea78b23c", + "sha256:fabd1f4d12dfd6b4f309208c2f31b116dc5900e0b42dbafe4ee1bc7c998ffbb0" + ], + "markers": "python_version >= '3.7'", + "version": "==7.2.4" }, "cron-descriptor": { "hashes": [ @@ -2645,11 +2645,11 @@ }, "flask": { "hashes": [ - "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", - "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d" + "sha256:13f6329ddbfff11340939cd11919daf150a01358ded4b7e81c03c055dfecb559", + "sha256:77504c4c097f56ac5f29b00f9009213010cf9d2923a288c0e0564a5db2bb53d6" ], "markers": "python_version >= '3.7'", - "version": "==2.2.3" + "version": "==2.2.4" }, "flask-appbuilder": { "hashes": [ @@ -2906,13 +2906,21 @@ "markers": "python_version >= '3.7'", "version": "==0.24.0" }, + "hypothesis": { + "hashes": [ + "sha256:3c5c344156a9e187f80dc5036c66110c9c4f98de8a280265f21895abba125d54", + "sha256:49b0a2288a609dd8920f8db05c10fb87e55a302154399bd4ee16e2e6e8f2cb8d" + ], + "markers": "python_version >= '3.7'", + "version": "==6.74.1" + }, "identify": { "hashes": [ - "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f", - "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e" + "sha256:17d9351c028a781456965e781ed2a435755cac655df1ebd930f7186b54399312", + "sha256:50b01b9d5f73c6b53e5fa2caf9f543d3e657a9d0bbdeb203ebb8d45960ba7433" ], "markers": "python_version >= '3.7'", - "version": "==2.5.22" + "version": "==2.5.23" }, "idna": { "hashes": [ @@ -2964,11 +2972,11 @@ }, "ipython": { "hashes": [ - "sha256:1c183bf61b148b00bcebfa5d9b39312733ae97f6dad90d7e9b4d86c8647f498c", - "sha256:a950236df04ad75b5bc7f816f9af3d74dc118fd42f2ff7e80e8e60ca1f182e2d" + "sha256:8d56026b882958db8eab089654f0c045d1237622313a1506da136fb0cce4270f", + "sha256:a0a8a30376cee8019c6e22bc0ab4320762f5f5e4d7abed0ea3ee4b95e3982ad5" ], "markers": "python_version >= '3.8'", - "version": "==8.12.0" + "version": "==8.13.0" }, "ipython-genutils": { "hashes": [ @@ -3455,19 +3463,19 @@ }, "nbclassic": { "hashes": [ - "sha256:47791b04dbcb89bf7fde910a3d848fd4793a4248a8936202453631a87da37d51", - "sha256:d2c91adc7909b0270c73e3e253d3687a6704b4f0a94bc156a37c85eba09f4d37" + "sha256:aab53fa1bea084fb6ade5c538b011a4f070c69f88d72878a8e8fb356f152509f", + "sha256:e3c8b7de80046c4a36a74662a5e325386d345289906c618366d8154e03dc2322" ], "markers": "python_version >= '3.7'", - "version": "==0.5.5" + "version": "==0.5.6" }, "nbclient": { "hashes": [ - "sha256:26e41c6dca4d76701988bc34f64e1bfc2413ae6d368f13d7b5ac407efb08c755", - "sha256:8fa96f7e36693d5e83408f5e840f113c14a45c279befe609904dbe05dad646d1" + "sha256:c817c0768c5ff0d60e468e017613e6eae27b6fa31e43f905addd2d24df60c125", + "sha256:d447f0e5a4cfe79d462459aec1b3dc5c2e9152597262be8ee27f7d4c02566a0d" ], "markers": "python_full_version >= '3.7.0'", - "version": "==0.7.3" + "version": "==0.7.4" }, "nbconvert": { "hashes": [ @@ -3592,11 +3600,11 @@ }, "platformdirs": { "hashes": [ - "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08", - "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e" + "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4", + "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335" ], "markers": "python_version >= '3.7'", - "version": "==3.2.0" + "version": "==3.5.0" }, "pluggy": { "hashes": [ @@ -4023,11 +4031,11 @@ }, "requests": { "hashes": [ - "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", - "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b", + "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059" ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.2" + "markers": "python_version >= '3.7'", + "version": "==2.29.0" }, "requests-toolbelt": { "hashes": [ @@ -4055,18 +4063,19 @@ }, "rich": { "hashes": [ - "sha256:22b74cae0278fd5086ff44144d3813be1cedc9115bdfabbfefd86400cb88b20a", - "sha256:b5d573e13605423ec80bdd0cd5f8541f7844a0e71a13f74cf454ccb2f490708b" + "sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c", + "sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.3.4" + "version": "==13.3.5" }, "send2trash": { "hashes": [ - "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d", - "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08" + "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679", + "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312" ], - "version": "==1.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.8.2" }, "setproctitle": { "hashes": [ @@ -4177,6 +4186,13 @@ ], "version": "==2.2.0" }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, "soupsieve": { "hashes": [ "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8", @@ -4187,11 +4203,11 @@ }, "sphinx": { "hashes": [ - "sha256:9ef22c2941bc3d0ff080d25a797f7521fc317e857395c712ddde97a19d5bb440", - "sha256:ff1c2a1167bef9cdcd8ec71339e85fe10f26d4e9ef9382ef10b2687c876c936b" + "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b", + "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912" ], "markers": "python_version >= '3.8'", - "version": "==6.2.0" + "version": "==6.2.1" }, "sphinx-autodoc-typehints": { "hashes": [ @@ -4322,11 +4338,11 @@ }, "sqlalchemy-utils": { "hashes": [ - "sha256:894cce255eea0bcc4fdcff628af30219d24a325526011586dd7f1e3d9dfebba0", - "sha256:986b4140f7740ff37244f6ed9182e8c997caa334150773de5932009b2490fb50" + "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801", + "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74" ], "markers": "python_version >= '3.6'", - "version": "==0.41.0" + "version": "==0.41.1" }, "sqlparse": { "hashes": [ @@ -4478,11 +4494,11 @@ }, "virtualenv": { "hashes": [ - "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3", - "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a" + "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e", + "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924" ], "markers": "python_version >= '3.7'", - "version": "==20.22.0" + "version": "==20.23.0" }, "vulture": { "hashes": [ @@ -4691,83 +4707,83 @@ }, "yarl": { "hashes": [ - "sha256:01a073c9175481dfed6b40704a1b67af5a9435fc4a58a27d35fd6b303469b0c7", - "sha256:01cf88cb80411978a14aa49980968c1aeb7c18a90ac978c778250dd234d8e0ba", - "sha256:08c8599d6aa8a24425f8635f6c06fa8726afe3be01c8e53e236f519bcfa5db5b", - "sha256:098bdc06ffb4db39c73883325b8c738610199f5f12e85339afedf07e912a39af", - "sha256:09c56a32c26e24ef98d5757c5064e252836f621f9a8b42737773aa92936b8e08", - "sha256:13a1ad1f35839b3bb5226f59816b71e243d95d623f5b392efaf8820ddb2b3cd5", - "sha256:1baf8cdaaab65d9ccedbf8748d626ad648b74b0a4d033e356a2f3024709fb82f", - "sha256:1d7a0075a55380b19aa43b9e8056e128b058460d71d75018a4f9d60ace01e78c", - "sha256:27efc2e324f72df02818cd72d7674b1f28b80ab49f33a94f37c6473c8166ce49", - "sha256:307a782736ebf994e7600dcaeea3b3113083584da567272f2075f1540919d6b3", - "sha256:395ea180257a3742d09dcc5071739682a95f7874270ebe3982d6696caec75be0", - "sha256:39a7a9108e9fc633ae381562f8f0355bb4ba00355218b5fb19cf5263fcdbfa68", - "sha256:3abe37fd89a93ebe0010417ca671f422fa6fcffec54698f623b09f46b4d4a512", - "sha256:4295790981630c4dab9d6de7b0f555a4c8defe3ed7704a8e9e595a321e59a0f5", - "sha256:44fa6158e6b4b8ccfa2872c3900a226b29e8ce543ce3e48aadc99816afa8874d", - "sha256:46c4010de941e2e1365c07fb4418ddca10fcff56305a6067f5ae857f8c98f3a7", - "sha256:4764114e261fe49d5df9b316b3221493d177247825c735b2aae77bc2e340d800", - "sha256:4d817593d345fefda2fae877accc8a0d9f47ada57086da6125fa02a62f6d1a94", - "sha256:518a92a34c741836a315150460b5c1c71ae782d569eabd7acf53372e437709f7", - "sha256:56956b13ec275de31fe4fb991510b735c4fb3e1b01600528c952b9ac90464430", - "sha256:575975d28795a61e82c85f114c02333ca54cbd325fd4e4b27598c9832aa732e7", - "sha256:5ce0bcab7ec759062c818d73837644cde567ab8aa1e0d6c45db38dfb7c284441", - "sha256:5faf3ec98747318cb980aaf9addf769da68a66431fc203a373d95d7ee9c1fbb4", - "sha256:65d952e464df950eed32bb5dcbc1b4443c7c2de4d7abd7265b45b1b3b27f5fa2", - "sha256:6b09cce412386ea9b4dda965d8e78d04ac5b5792b2fa9cced3258ec69c7d1c16", - "sha256:6cdb47cbbacae8e1d7941b0d504d0235d686090eef5212ca2450525905e9cf02", - "sha256:6cf47fe9df9b1ededc77e492581cdb6890a975ad96b4172e1834f1b8ba0fc3ba", - "sha256:73a4b46689f2d59c8ec6b71c9a0cdced4e7863dd6eb98a8c30ea610e191f9e1c", - "sha256:74390c2318d066962500045aa145f5412169bce842e734b8c3e6e3750ad5b817", - "sha256:75676110bce59944dd48fd18d0449bd37eaeb311b38a0c768f7670864b5f8b68", - "sha256:78755ce43b6e827e65ec0c68be832f86d059fcf05d4b33562745ebcfa91b26b1", - "sha256:791357d537a09a194f92b834f28c98d074e7297bac0a8f1d5b458a906cafa17c", - "sha256:85aa6fd779e194901386709e0eedd45710b68af2709f82a84839c44314b68c10", - "sha256:88f6413ff5edfb9609e2769e32ce87a62353e66e75d264bf0eaad26fb9daa8f2", - "sha256:89099c887338608da935ba8bee027564a94f852ac40e472de15d8309517ad5fe", - "sha256:89da1fd6068553e3a333011cc17ad91c414b2100c32579ddb51517edc768b49c", - "sha256:8c72a1dc7e2ea882cd3df0417c808ad3b69e559acdc43f3b096d67f2fb801ada", - "sha256:90ebaf448b5f048352ec7c76cb8d452df30c27cb6b8627dfaa9cf742a14f141a", - "sha256:92a101f6d5a9464e86092adc36cd40ef23d18a25bfb1eb32eaeb62edc22776bb", - "sha256:92e37999e36f9f3ded78e9d839face6baa2abdf9344ea8ed2735f495736159de", - "sha256:97d76a3128f48fa1c721ef8a50e2c2f549296b2402dc8a8cde12ff60ed922f53", - "sha256:9ba5a18c4fbd408fe49dc5da85478a76bc75c1ce912d7fd7b43ed5297c4403e1", - "sha256:9bb794882818fae20ff65348985fdf143ea6dfaf6413814db1848120db8be33e", - "sha256:a21789bdf28549d4eb1de6910cabc762c9f6ae3eef85efc1958197c1c6ef853b", - "sha256:a8b8d4b478a9862447daef4cafc89d87ea4ed958672f1d11db7732b77ead49cc", - "sha256:ac8e593df1fbea820da7676929f821a0c7c2cecb8477d010254ce8ed54328ea8", - "sha256:b20a5ddc4e243cbaa54886bfe9af6ffc4ba4ef58f17f1bb691e973eb65bba84d", - "sha256:b2b2382d59dec0f1fdca18ea429c4c4cee280d5e0dbc841180abb82e188cf6e9", - "sha256:b3b5f8da07a21f2e57551f88a6709c2d340866146cf7351e5207623cfe8aad16", - "sha256:b5d5fb6c94b620a7066a3adb7c246c87970f453813979818e4707ac32ce4d7bd", - "sha256:b63d41e0eecf3e3070d44f97456cf351fff7cb960e97ecb60a936b877ff0b4f6", - "sha256:b86e98c3021b7e2740d8719bf074301361bf2f51221ca2765b7a58afbfbd9042", - "sha256:bab67d041c78e305ff3eef5e549304d843bd9b603c8855b68484ee663374ce15", - "sha256:c3ca8d71b23bdf164b36d06df2298ec8a5bd3de42b17bf3e0e8e6a7489195f2c", - "sha256:ca14b84091700ae7c1fcd3a6000bd4ec1a3035009b8bcb94f246741ca840bb22", - "sha256:d21887cbcf6a3cc5951662d8222bc9c04e1b1d98eebe3bb659c3a04ed49b0eec", - "sha256:d5c407e530cf2979ea383885516ae79cc4f3c3530623acf5e42daf521f5c2564", - "sha256:d966cd59df9a4b218480562e8daab39e87e746b78a96add51a3ab01636fc4291", - "sha256:df747104ef27ab1aa9a1145064fa9ea26ad8cf24bfcbdba7db7abf0f8b3676b9", - "sha256:e124b283a04cc06d22443cae536f93d86cd55108fa369f22b8fe1f2288b2fe1c", - "sha256:e2f01351b7809182822b21061d2a4728b7b9e08f4585ba90ee4c5c4d3faa0812", - "sha256:e7ddebeabf384099814353a2956ed3ab5dbaa6830cc7005f985fcb03b5338f05", - "sha256:e9fe3a1c073ab80a28a06f41d2b623723046709ed29faf2c56bea41848597d86", - "sha256:ecaa5755a39f6f26079bf13f336c67af589c222d76b53cd3824d3b684b84d1f1", - "sha256:ecad20c3ef57c513dce22f58256361d10550a89e8eaa81d5082f36f8af305375", - "sha256:eed9827033b7f67ad12cb70bd0cb59d36029144a7906694317c2dbf5c9eb5ddd", - "sha256:ef7e2f6c47c41e234600a02e1356b799761485834fe35d4706b0094cb3a587ee", - "sha256:efec77851231410125cb5be04ec96fa4a075ca637f415a1f2d2c900b09032a8a", - "sha256:f0cd87949d619157a0482c6c14e5011f8bf2bc0b91cb5087414d9331f4ef02dd", - "sha256:f206adb89424dca4a4d0b31981869700e44cd62742527e26d6b15a510dd410a2", - "sha256:f5bcb80006efe9bf9f49ae89711253dd06df8053ff814622112a9219346566a7", - "sha256:f76edb386178a54ea7ceffa798cb830c3c22ab50ea10dfb25dc952b04848295f", - "sha256:f878a78ed2ccfbd973cab46dd0933ecd704787724db23979e5731674d76eb36f", - "sha256:f8e73f526140c1c32f5fca4cd0bc3b511a1abcd948f45b2a38a95e4edb76ca72" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.1" + "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571", + "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3", + "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3", + "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c", + "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7", + "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04", + "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191", + "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea", + "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4", + "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4", + "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095", + "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e", + "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74", + "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef", + "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33", + "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde", + "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45", + "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf", + "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b", + "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac", + "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0", + "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528", + "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716", + "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb", + "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18", + "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72", + "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6", + "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582", + "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5", + "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368", + "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc", + "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9", + "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be", + "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a", + "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80", + "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8", + "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6", + "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417", + "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574", + "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59", + "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608", + "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82", + "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1", + "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3", + "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d", + "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8", + "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc", + "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac", + "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8", + "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955", + "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0", + "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367", + "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb", + "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a", + "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623", + "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2", + "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6", + "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7", + "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4", + "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051", + "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938", + "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8", + "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9", + "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3", + "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5", + "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9", + "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333", + "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185", + "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3", + "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560", + "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b", + "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7", + "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78", + "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.2" }, "ypy-websocket": { "hashes": [ diff --git a/setup.cfg b/setup.cfg index 77409d6..3239f64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -94,6 +94,7 @@ dev = jupyterlab~=3.6 vulture~=2.7 autopep8~=2.0 + typing-extensions~=4.5 [options.entry_points] # Add here console scripts like: @@ -118,7 +119,7 @@ apache_airflow_provider = # Comment those flags to avoid this pytest issue. addopts = --cov "orca" --cov-report "term-missing" --cov-report "xml" - -m "not slow and not integration and not acceptance" + -m "not slow and not integration and not acceptance and not cost" --verbose norecursedirs = dist @@ -135,6 +136,7 @@ markers = slow: mark tests as slow (deselect with '-m "not slow"') integration: mark tests that interact with external services acceptance: mark end-to-end acceptance tests + cost: mark tests that have costs associated with them [devpi:upload] # Options for the devpi: PyPI server and packaging tool diff --git a/src/orca/services/nextflowtower/client.py b/src/orca/services/nextflowtower/client.py index 1aa59f5..4192d36 100644 --- a/src/orca/services/nextflowtower/client.py +++ b/src/orca/services/nextflowtower/client.py @@ -1,11 +1,12 @@ -from typing import Any +from typing import Any, Optional import requests from pydantic.dataclasses import dataclass from requests.exceptions import HTTPError +from orca.services.nextflowtower import models + -# TODO: Consider creating a `client` submodule folder to organize methods @dataclass(kw_only=False) class NextflowTowerClient: """Simple Python client for making requests to Nextflow Tower. @@ -85,105 +86,241 @@ def request_json(self, method: str, path: str, **kwargs) -> dict[str, Any]: response.raise_for_status() return response.json() - # def request_paged(self, method: str, path: str, **kwargs) -> list[dict[str, Any]]: - # """Iterate through pages of results for a given request. - - # See ``TowerClient.request`` for argument definitions. + def request_paged(self, method: str, path: str, **kwargs) -> dict[str, Any]: + """Iterate through pages of results for a given request. - # Raises: - # HTTPError: If the response doesn't match the expectation - # for a paged endpoint. - - # Returns: - # The cumulative list of items from all pages. - # """ - # self.update_kwarg(kwargs, "params", "max", 50) - # self.update_kwarg(kwargs, "params", "offset", 0) + See ``TowerClient.request`` for argument definitions. - # num_items = 0 - # total_size = 1 # Artificial value for initiating the while-loop + Raises: + HTTPError: If the response doesn't match the expectation + for a paged endpoint. - # all_items = list() - # while num_items < total_size: - # kwargs["params"]["offset"] = num_items - # json = self.request_json(method, path, **kwargs) + Returns: + The cumulative list of items from all pages. + """ + # Ensure defaults for pagination query parameters + self.update_kwarg(kwargs, "params", "max", 50) + self.update_kwarg(kwargs, "params", "offset", 0) + + num_items = 0 + all_items = list() + key_name = "items" # Setting a default value + total_size = float("inf") # Artificial value for initiating the while-loop + while num_items < total_size: + kwargs["params"]["offset"] = num_items + json = self.request_json(method, path, **kwargs) + total_size = json.pop("totalSize") + key_name, items = json.popitem() + num_items += len(items) + all_items.extend(items) + + if len(all_items) != total_size: + message = f"Expected {total_size} items, but got: {all_items}" + raise HTTPError(message) - # if "totalSize" not in json: - # message = f"'totalSize' not in response JSON ({json}) as expected." - # raise HTTPError(message) - # total_size = json.pop("totalSize") + json = {"totalSize": total_size, key_name: all_items} + return json - # if len(json) != 1: - # message = f"Expected one other key aside from 'totalSize' ({json})." - # raise HTTPError(message) - # _, items = json.popitem() + def get(self, path: str, **kwargs) -> dict[str, Any]: + """Send an auth'ed GET request and parse the JSON response. - # num_items += len(items) - # all_items.extend(items) + See ``TowerClient.request`` for argument definitions. - # return all_items + Returns: + A dictionary from deserializing the JSON response. + """ + json = self.request_json("GET", path, **kwargs) + if "totalSize" in json: + json = self.request_paged("GET", path, **kwargs) + return json - def get(self, path: str, **kwargs) -> dict[str, Any]: - """Send an auth'ed GET request and parse the JSON response. + def post(self, path: str, **kwargs) -> dict[str, Any]: + """Send an auth'ed POST request and parse the JSON response. See ``TowerClient.request`` for argument definitions. Returns: A dictionary from deserializing the JSON response. """ - return self.request_json("GET", path, **kwargs) + return self.request_json("POST", path, **kwargs) - def get_user_info(self) -> dict[str, Any]: - """Describe current user. + def unwrap(self, json: dict[str, Any], key: str) -> Any: + """Unwrap nested key in JSON response. - Raises: - HTTPError: If the response lacks the expected keys. + Args: + json: Raw JSON response. + key: Top-level key. Returns: - Current user. + Unnested JSON response. """ - path = "/user-info" - key = "user" - response = self.get(path) - if key not in response: - message = f"Expecting '{key}' key in response ({response})." + if key not in json: + message = f"Expecting '{key}' key in JSON response ({json})." raise HTTPError(message) - return response[key] + return json[key] - def get_user_workspaces_and_orgs(self, user_id: int) -> list[dict[str, Any]]: - """List the workspaces and organizations of a given user. + def get_user_info(self) -> models.User: + """Describe current user. - Raises: - HTTPError: If the response lacks the expected keys. + Returns: + Current user. + """ + path = "/user-info" + json = self.get(path) + unwrapped = self.unwrap(json, "user") + return models.User.from_json(unwrapped) + + def list_user_workspaces_and_orgs( + self, + user_id: int, + ) -> list[models.Workspace | models.Organization]: + """List the workspaces and organizations of a given user. Returns: - Workspaces and organizations. + List of workspaces and organizations. """ path = f"/user/{user_id}/workspaces" - key = "orgsAndWorkspaces" - response = self.get(path) - if key not in response: - message = f"Expecting '{key}' key in response ({response})." - raise HTTPError(message) - return response[key] - - # TODO: Should this higher-level method exist here or in Ops? - def list_user_workspaces(self) -> list[dict[str, Any]]: + json = self.get(path) + items = self.unwrap(json, "orgsAndWorkspaces") + objects: list[models.Organization | models.Workspace] = list() + for item in items: + if item["workspaceId"]: + workspace = models.Workspace.from_json(item) + objects.append(workspace) + else: + org = models.Organization.from_json(item) + objects.append(org) + return objects + + def list_user_workspaces(self) -> list[models.Workspace]: """List the workspaces that are available to the current user. Returns: List of user workspaces. """ user = self.get_user_info() - orgs_and_workspaces = self.get_user_workspaces_and_orgs(user["id"]) + items = self.list_user_workspaces_and_orgs(user.id) + return [item for item in items if isinstance(item, models.Workspace)] - workspaces = list() - for workspace in orgs_and_workspaces: - # Response includes organizations, which don't have workspace IDs - if workspace["workspaceId"] is None: - continue - workspaces.append(workspace) - return workspaces + def generate_params( + self, + workspace_id: Optional[int], + **kwargs, + ) -> dict[str, Any]: + """Generate URL query parameters. + + Args: + workspace_id: Tower workspace ID. + **kwargs: Additional query parameters that are included + if they are not set to None. + + Returns: + URL query parameters based on input. + """ + params = {} + if workspace_id: + params["workspaceId"] = int(workspace_id) + for name, value in kwargs.items(): + if value is not None: + params[name] = value + return params + + def get_compute_env( + self, + compute_env_id: str, + workspace_id: Optional[int] = None, + ) -> models.ComputeEnv: + """Retrieve information about a given compute environment. + + Args: + compute_env_id: Compute environment ID. + workspace_id: Tower workspace ID. + + Returns: + Compute environment instance. + """ + path = f"/compute-envs/{compute_env_id}" + params = self.generate_params(workspace_id, attributes="labels") + json = self.get(path, params=params) + unwrapped = self.unwrap(json, "computeEnv") + return models.ComputeEnv.from_json(unwrapped) + + def list_compute_envs( + self, + workspace_id: Optional[int] = None, + status: Optional[str] = None, + ) -> list[models.ComputeEnvSummary]: + """List all compute environments. + + Args: + workspace_id: Tower workspace ID. Defaults to None. + status: Compute environment status to filter on. + Defaults to None. + + Returns: + List of compute environments. + """ + path = "/compute-envs" + params = self.generate_params(workspace_id, status=status) + json = self.get(path, params=params) + items = self.unwrap(json, "computeEnvs") + return [models.ComputeEnvSummary.from_json(item) for item in items] + + def create_label( + self, + name: str, + workspace_id: Optional[int] = None, + ) -> models.Label: + """Create a workflow label. + + Args: + name: Label name. + workspace_id: Tower workspace ID. Defaults to None. + + Returns: + Label instance. + """ + path = "/labels" + params = self.generate_params(workspace_id) + payload = {"name": name, "resource": False} + json = self.post(path, params=params, json=payload) + return models.Label.from_json(json) + + def list_labels(self, workspace_id: Optional[int] = None) -> list[models.Label]: + """List all available labels. + + Args: + workspace_id: Tower workspace ID. Defaults to None. + + Returns: + List of available labels. + """ + path = "/labels" + params = self.generate_params(workspace_id) + json = self.get(path, params=params) + items = self.unwrap(json, "labels") + return [models.Label.from_json(item) for item in items] + + def launch_workflow( + self, + launch_info: models.LaunchInfo, + workspace_id: Optional[int] = None, + ) -> str: + """Launch a workflow in the target workspace. + + Args: + launch_info: Description of which workflow to + launch and how, including input parameters. + workspace_id: Tower workspace ID. + + Returns: + Workflow run ID. + """ + path = "/workflow/launch" + params = self.generate_params(workspace_id) + payload = launch_info.to_dict() + json = self.post(path, params=params, json=payload) + return self.unwrap(json, "workflowId") def get_workflow(self, workspace_id: int, workflow_id: str) -> dict: """Gets available information about a workflow run @@ -198,5 +335,5 @@ def get_workflow(self, workspace_id: int, workflow_id: str) -> dict: response (dict): Dictionary containing information about the workflow run """ path = f"/workflow/{workflow_id}" - response = self.get(path=path, params={"workspaceId": workspace_id}) - return response + json = self.get(path=path, params={"workspaceId": workspace_id}) + return json diff --git a/src/orca/services/nextflowtower/models.py b/src/orca/services/nextflowtower/models.py new file mode 100644 index 0000000..f3505a5 --- /dev/null +++ b/src/orca/services/nextflowtower/models.py @@ -0,0 +1,236 @@ +import json +from collections.abc import Collection +from dataclasses import field +from datetime import datetime +from enum import Enum +from typing import Any, Optional + +from pydantic.dataclasses import dataclass +from typing_extensions import Self + +from orca.services.nextflowtower.utils import parse_datetime + + +class TaskStatus(Enum): + """enum containing all possible status values for + Nextflow Tower runs. terminal_states set which + statuses result in a run being determined to be + "complete" + """ + + SUBMITTED = "SUBMITTED" + RUNNING = "RUNNING" + SUCCEEDED = "SUCCEEDED" + FAILED = "FAILED" + CANCELLED = "CANCELLED" + UNKNOWN = "UNKNOWN" + + terminal_states = [SUCCEEDED, FAILED, CANCELLED, UNKNOWN] + + +@dataclass(kw_only=False) +class User: + """Nextflow Tower user.""" + + id: int + username: str + email: str + + @classmethod + def from_json(cls, response: dict[str, Any]) -> Self: + """Create user from API JSON response. + + Returns: + User instance. + """ + return cls(response["id"], response["userName"], response["email"]) + + +@dataclass(kw_only=False) +class Organization: + """Nextflow Tower organization.""" + + id: int + name: str + + @classmethod + def from_json(cls, response: dict[str, Any]) -> Self: + """Create organization from API JSON response. + + Returns: + Organization instance. + """ + return cls(response["orgId"], response["orgName"]) + + +@dataclass(kw_only=False) +class Workspace: + """Nextflow Tower workspace.""" + + id: int + name: str + org: Organization + + @property + def full_name(self) -> str: + """Fully-qualified workspace name (with organization name).""" + return f"{self.org.name}/{self.name}".lower() + + @classmethod + def from_json(cls, response: dict[str, Any]) -> Self: + """Create workspace from API JSON response. + + Returns: + Workspace instance. + """ + org = Organization.from_json(response) + return cls(response["workspaceId"], response["workspaceName"], org) + + +@dataclass(kw_only=False) +class LaunchInfo: + """Nextflow Tower workflow launch specification.""" + + compute_env_id: str + pipeline: str + work_dir: str + revision: Optional[str] = None + params: Optional[dict] = None + nextflow_config: Optional[str] = None + run_name: Optional[str] = None + pre_run_script: Optional[str] = None + profiles: list[str] = field(default_factory=list) + user_secrets: list[str] = field(default_factory=list) + workspace_secrets: list[str] = field(default_factory=list) + label_ids: list[int] = field(default_factory=list) + + @staticmethod + def dedup(items: Collection[str]) -> list[str]: + """Deduplicate items in a collection. + + Args: + items: Collection of items. + + Returns: + Deduplicated collection or None. + """ + return list(set(items)) + + def fill_in(self, attr: str, value: Any): + """Fill in any missing values. + + Args: + attr: Attribute name. + value: Attribute value. + """ + if not getattr(self, attr, None): + setattr(self, attr, value) + + def to_dict(self) -> dict[str, Any]: + """Generate JSON representation of a launch specification. + + Returns: + JSON representation. + """ + output = { + "launch": { + "computeEnvId": self.compute_env_id, + "configProfiles": self.dedup(self.profiles), + "configText": self.nextflow_config, + "dateCreated": None, + "entryName": None, + "headJobCpus": None, + "headJobMemoryMb": None, + "id": None, + "labelIds": self.label_ids, + "mainScript": None, + "optimizationId": None, + "paramsText": json.dumps(self.params), + "pipeline": self.pipeline, + "postRunScript": None, + "preRunScript": self.pre_run_script, + "pullLatest": False, + "resume": False, + "revision": self.revision, + "runName": self.run_name, + "schemaName": None, + "stubRun": False, + "towerConfig": None, + "userSecrets": self.dedup(self.user_secrets), + "workDir": self.work_dir, + "workspaceSecrets": self.dedup(self.workspace_secrets), + } + } + return output + + +@dataclass(kw_only=False) +class Label: + """Nextflow Tower workflow run label.""" + + id: int + name: str + value: Optional[str] + resource: bool + + @classmethod + def from_json(cls, response: dict[str, Any]) -> Self: + """Create label from API JSON response. + + Returns: + Label instance. + """ + return cls(**response) + + +@dataclass(kw_only=False) +class ComputeEnvSummary: + """Nextflow Tower compute environment summary.""" + + id: str + name: str + status: str + work_dir: str + raw: dict + + @classmethod + def from_json(cls, response: dict[str, Any]) -> Self: + """Create compute environment from API JSON response. + + Returns: + Compute environment instance. + """ + return cls( + response["id"], + response["name"], + response["status"], + response["workDir"], + response, + ) + + +@dataclass(kw_only=False) +class ComputeEnv(ComputeEnvSummary): + """Nextflow Tower compute environment details.""" + + date_created: datetime + pre_run_script: str + labels: list[Label] + + @classmethod + def from_json(cls, response: dict[str, Any]) -> Self: + """Create compute environment from API JSON response. + + Returns: + Compute environment instance. + """ + return cls( + id=response["id"], + name=response["name"], + status=response["status"], + work_dir=response["config"]["workDir"], + date_created=parse_datetime(response["dateCreated"]), + pre_run_script=response["config"]["preRunScript"], + labels=[Label.from_json(label) for label in response["labels"]], + raw=response, + ) diff --git a/src/orca/services/nextflowtower/models/enums.py b/src/orca/services/nextflowtower/models/enums.py deleted file mode 100644 index 3165ed2..0000000 --- a/src/orca/services/nextflowtower/models/enums.py +++ /dev/null @@ -1,18 +0,0 @@ -from enum import Enum - - -class TaskStatus(Enum): - """enum containing all possible status values for - Nextflow Tower runs. terminal_states set which - statuses result in a run being determined to be - "complete" - """ - - SUBMITTED = "SUBMITTED" - RUNNING = "RUNNING" - SUCCEEDED = "SUCCEEDED" - FAILED = "FAILED" - CANCELLED = "CANCELLED" - UNKNOWN = "UNKNOWN" - - terminal_states = [SUCCEEDED, FAILED, CANCELLED, UNKNOWN] diff --git a/src/orca/services/nextflowtower/ops.py b/src/orca/services/nextflowtower/ops.py index 3b1fb56..1039e90 100644 --- a/src/orca/services/nextflowtower/ops.py +++ b/src/orca/services/nextflowtower/ops.py @@ -1,13 +1,14 @@ from functools import cached_property -from typing import cast +from typing import ClassVar, Optional, cast from pydantic.dataclasses import dataclass from orca.errors import ConfigError from orca.services.base.ops import BaseOps +from orca.services.nextflowtower.client import NextflowTowerClient from orca.services.nextflowtower.client_factory import NextflowTowerClientFactory from orca.services.nextflowtower.config import NextflowTowerConfig -from orca.services.nextflowtower.models.enums import TaskStatus +from orca.services.nextflowtower.models import LaunchInfo, TaskStatus @dataclass(kw_only=False) @@ -25,14 +26,17 @@ class NextflowTowerOps(BaseOps): client_factory_class = NextflowTowerClientFactory + client: ClassVar[NextflowTowerClient] + + launch_label: ClassVar[str] = "launched-by-orca" + @cached_property def workspace_id(self) -> int: """The currently active Nextflow Tower workspace ID.""" workspaces = self.client.list_user_workspaces() for workspace in workspaces: - full_name = f"{workspace['orgName']}/{workspace['workspaceName']}" - if full_name.lower() == self.workspace.lower(): - return workspace["workspaceId"] + if workspace.full_name == self.workspace: + return workspace.id message = f"Workspace ({self.workspace}) not available to user ({workspaces})." raise ValueError(message) @@ -42,7 +46,90 @@ def workspace(self) -> str: if self.config.workspace is None: message = f"Config ({self.config}) does not specify a workspace." raise ConfigError(message) - return self.config.workspace + return self.config.workspace.lower() + + def get_latest_compute_env(self, filter: Optional[str] = None) -> str: + """Get latest available compute environment matching filter. + + Args: + filter: A string to filter compute environment names. + For example, "ondemand" for filtering for on-demand + compute environments. Default to None, which doesn't + apply any filtering. + + Raises: + ValueError: If no matching compute environments exist. + + Returns: + Compute environment ID. + """ + envs = self.client.list_compute_envs(self.workspace_id, "AVAILABLE") + if filter: + envs = [env for env in envs if filter in env.name] + if len(envs) == 0: + message = f"No matching compute environments ({filter=})." + raise ValueError(message) + elif len(envs) == 1: + return envs[0].id + + # Fill in additional info and sort by dateCreated if there are multiple matches) + all_details = list() + for env in envs: + details = self.client.get_compute_env(env.id, self.workspace_id) + all_details.append(details) + all_details = sorted(all_details, key=lambda x: x.date_created) + latest_env = all_details[-1] + return latest_env.id + + def create_label(self, name: str) -> int: + """Create (or get existing) workflow (non-resource) label. + + Args: + name: Label name. + + Returns: + Label ID. + """ + labels = self.client.list_labels(self.workspace_id) + for label in labels: + if label.resource: + continue + if label.name == name: + return label.id + label = self.client.create_label(name, self.workspace_id) + return label.id + + # TODO: Once get_workflow() is available, try to make idempotent + def launch_workflow( + self, + launch_info: LaunchInfo, + compute_env_filter: Optional[str] = None, + ) -> str: + """Launch a workflow using the latest matching compute env. + + Args: + launch_info: Workflow launch information. + compute_env_filter: Filter for matching compute + environments. Default to None. + + Returns: + Workflow run ID. + """ + compute_env_id = self.get_latest_compute_env(compute_env_filter) + compute_env = self.client.get_compute_env(compute_env_id, self.workspace_id) + label_ids = [label.id for label in compute_env.labels] + + # Ensure that all workflows are labeled for easy querying + query_label_id = self.create_label(self.launch_label) + label_ids.append(query_label_id) + + # Update launch_info with compute_env defaults and label ID + launch_info.fill_in("compute_env_id", compute_env_id) + launch_info.fill_in("work_dir", compute_env.work_dir) + launch_info.fill_in("pre_run_script", compute_env.pre_run_script) + launch_info.fill_in("label_ids", label_ids) + + return self.client.launch_workflow(launch_info, self.workspace_id) def get_workflow_status(self, workflow_id: str) -> tuple[TaskStatus, bool]: """Gets status of workflow run @@ -59,5 +146,5 @@ def get_workflow_status(self, workflow_id: str) -> tuple[TaskStatus, bool]: ) task_status = cast(TaskStatus, response["workflow"]["status"]) is_done = task_status in TaskStatus.terminal_states.value - # TODO consider switching return value to a namedtuple + # TODO: Consider switching return value to a namedtuple return task_status, is_done diff --git a/src/orca/services/nextflowtower/utils.py b/src/orca/services/nextflowtower/utils.py new file mode 100644 index 0000000..7251307 --- /dev/null +++ b/src/orca/services/nextflowtower/utils.py @@ -0,0 +1,15 @@ +from datetime import datetime, timezone + + +def parse_datetime(text: str) -> datetime: + """Parse Tower datetime strings (RFC 3339). + + Args: + text: Datetime string. + + Returns: + Datetime object. + """ + parsed = datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ") + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed diff --git a/tests/acceptance/test_cavatica.py b/tests/acceptance/test_cavatica.py index bfc2bfc..3730128 100644 --- a/tests/acceptance/test_cavatica.py +++ b/tests/acceptance/test_cavatica.py @@ -4,6 +4,7 @@ from orca.services.sevenbridges import SevenBridgesHook +@pytest.mark.cost @pytest.mark.acceptance def test_cavatica_launch_poc_v2(run_id): def create_task(): diff --git a/tests/services/nextflowtower/conftest.py b/tests/services/nextflowtower/conftest.py index 6f36d38..ebc27bc 100644 --- a/tests/services/nextflowtower/conftest.py +++ b/tests/services/nextflowtower/conftest.py @@ -34,7 +34,7 @@ def mocked_ops(config, client, mocker): yield NextflowTowerOps(config) -@pytest.fixture +@pytest.fixture(scope="session") def get_response(): def _get_response(name: str) -> dict: response = getattr(responses, name) diff --git a/tests/services/nextflowtower/responses.py b/tests/services/nextflowtower/responses.py index 5ca73ee..187c283 100644 --- a/tests/services/nextflowtower/responses.py +++ b/tests/services/nextflowtower/responses.py @@ -2,8 +2,8 @@ To generate these, you can visit the OpenAPI page linked below and make example requests after setting your authentication token at the -top of the page. You'll have to replace JSON literals ('null', 'false', -'true') with Python equivalents ('None', 'False', 'True'). Take care +top of the page. You'll have to replace JSON literals ('None', 'False', +'True') with Python equivalents ('None', 'False', 'True'). Take care of redacting any values that shouldn't be public. https://tower-dev.sagebionetworks.org/api/openapi @@ -41,21 +41,21 @@ get_user_workspaces_and_orgs = { "orgsAndWorkspaces": [ { - "orgId": 123456789, + "orgId": 12345, "orgName": "Foo-Bar", "orgLogoUrl": None, "workspaceId": None, "workspaceName": None, }, { - "orgId": 123456789, + "orgId": 12345, "orgName": "Foo-Bar", "orgLogoUrl": None, "workspaceId": 54321, "workspaceName": "project-1", }, { - "orgId": 123456789, + "orgId": 12345, "orgName": "Foo-Bar", "orgLogoUrl": None, "workspaceId": 98765, @@ -64,6 +64,156 @@ ] } + +create_label = {"id": 12345, "name": "foo", "value": None, "resource": False} + + +list_labels = { + "labels": [ + {"id": 89366, "name": "CostCenter", "value": "12345", "resource": True}, + {"id": 10567, "name": "Department", "value": "IBC", "resource": True}, + {"id": 17863, "name": "launched-by-orca", "value": None, "resource": False}, + {"id": 97881, "name": "ORCA-163", "value": None, "resource": False}, + {"id": 18898, "name": "Project", "value": "Infrastructure", "resource": True}, + ], + "totalSize": 5, +} + + +get_compute_env: dict = { + "computeEnv": { + "id": "5ykJF", + "name": "orca-service-test-project-ondemand-v11", + "description": None, + "platform": "aws-batch", + "config": { + "region": "us-east-1", + "computeQueue": "TowerForge-5ykJF", + "dragenQueue": None, + "computeJobRole": "foo", + "executionRole": "foo", + "headQueue": "TowerForge-5ykJF", + "headJobRole": "foo", + "cliPath": "/home/ec2-user/miniconda/bin/aws", + "volumes": [], + "workDir": "s3://orca-service-test-project-tower-scratch/work", + "preRunScript": "NXF_OPTS='-Xms7g -Xmx14g'", + "postRunScript": None, + "headJobCpus": 8, + "headJobMemoryMb": 15000, + "environment": None, + "waveEnabled": False, + "fusion2Enabled": False, + "nvnmeStorageEnabled": False, + "logsGroup": None, + "forge": { + "type": "EC2", + "minCpus": 0, + "maxCpus": 1000, + "gpuEnabled": False, + "ebsAutoScale": True, + "instanceTypes": [ + "c5a.large", + "m6a.large", + "r6a.large", + ], + "allocStrategy": "BEST_FIT", + "imageId": None, + "vpcId": "vpc-100", + "subnets": [ + "subnet-1", + "subnet-2", + "subnet-3", + "subnet-4", + ], + "securityGroups": [], + "fsxMount": None, + "fsxName": None, + "fsxSize": None, + "disposeOnDeletion": True, + "ec2KeyPair": None, + "allowBuckets": [], + "ebsBlockSize": 1000, + "fusionEnabled": None, + "bidPercentage": None, + "efsCreate": False, + "efsId": None, + "efsMount": None, + "dragenEnabled": None, + "dragenAmiId": None, + "ebsBootSize": 1000, + "ecsConfig": "foo", + }, + "forgedResources": [ + {"IamRole": "foo"}, + {"IamRole": "foo"}, + {"IamInstanceProfile": "foo"}, + {"Ec2LaunchTemplate": "TowerForge-5ykJF"}, + {"BatchEnv": "foo"}, + {"BatchQueue": "foo"}, + ], + "discriminator": "aws-batch", + }, + "dateCreated": "2023-04-26T00:49:49Z", + "lastUpdated": "2023-04-26T00:50:17Z", + "lastUsed": "2023-04-27T23:44:45Z", + "deleted": None, + "status": "AVAILABLE", + "message": None, + "primary": None, + "credentialsId": "S2AIo", + "orgId": 12345, + "workspaceId": 98765, + "labels": list_labels["labels"], + } +} + + +list_compute_envs = { + "computeEnvs": [ + { + "id": "3QGDs", + "name": "orca-service-test-project-spot-v11", + "platform": "aws-batch", + "status": "AVAILABLE", + "message": None, + "lastUsed": None, + "primary": True, + "workspaceName": "orca-service-test-project", + "visibility": "PRIVATE", + "workDir": "s3://orca-service-test-project-tower-scratch/work", + }, + { + "id": "5ykJF", + "name": "orca-service-test-project-ondemand-v11", + "platform": "aws-batch", + "status": "AVAILABLE", + "message": None, + "lastUsed": "2023-04-27T23:44:45Z", + "primary": None, + "workspaceName": "orca-service-test-project", + "visibility": "PRIVATE", + "workDir": "s3://orca-service-test-project-tower-scratch/work", + }, + { + "id": "9W2l7", + "name": "orca-service-test-project-ondemand-v10", + "platform": "aws-batch", + "status": "AVAILABLE", + "message": None, + "lastUsed": "2023-01-20T18:27:12Z", + "primary": None, + "workspaceName": "orca-service-test-project", + "visibility": "PRIVATE", + "workDir": "s3://orca-service-test-project-tower-scratch/work", + }, + ] +} + + +launch_workflow = {"workflowId": "23LNH"} + + get_workflow: dict = { "workflow": { "id": "123456789", diff --git a/tests/services/nextflowtower/test_client.py b/tests/services/nextflowtower/test_client.py index e5aa73a..3e9a88a 100644 --- a/tests/services/nextflowtower/test_client.py +++ b/tests/services/nextflowtower/test_client.py @@ -1,6 +1,8 @@ import pytest from requests.exceptions import HTTPError +from orca.services.nextflowtower import models + def test_that_update_kwargs_updates_an_empty_dictionary(client): kwargs = {} @@ -36,7 +38,7 @@ def test_that_get_user_info_works(client, mocker, get_response): mock.return_value = expected actual = client.get_user_info() mock.assert_called_once() - assert actual == expected["user"] + assert actual == models.User.from_json(expected["user"]) def test_that_get_user_info_fails_with_nonstandard_response(client, mocker): @@ -63,6 +65,73 @@ def test_that_list_user_workspaces_fails_with_nonstandard_response(client, mocke client.list_user_workspaces() +def test_that_get_compute_env_works(client, mocker, get_response): + mock = mocker.patch.object(client, "request_json") + mock.return_value = get_response("get_compute_env") + response = client.get_compute_env("5ykJF", 98765) + mock.assert_called_once() + assert response.id == "5ykJF" + + +def test_that_list_compute_envs_works(client, mocker, get_response): + mock = mocker.patch.object(client, "request_json") + mock.return_value = get_response("list_compute_envs") + response = client.list_compute_envs(98765) + mock.assert_called_once() + assert len(response) == 3 + + +def test_that_list_compute_envs_works_with_status_filter(client, mocker, get_response): + mock = mocker.patch.object(client, "request_json") + mock.return_value = get_response("list_compute_envs") + response = client.list_compute_envs(98765, "AVAILABLE") + mock.assert_called_once() + assert len(response) == 3 + + +def test_that_create_label_works(client, mocker, get_response): + mock = mocker.patch.object(client, "request_json") + mock.return_value = get_response("create_label") + response = client.create_label("foo", 98765) + mock.assert_called_once() + assert response.id == 12345 + + +def test_that_list_labels_works(client, mocker, get_response): + full_response = get_response("list_labels") + page_1 = {"totalSize": 5, "labels": full_response["labels"][:3]} + page_2 = {"totalSize": 5, "labels": full_response["labels"][3:]} + mock = mocker.patch.object(client, "request_json") + mock.side_effect = [page_1, page_1, page_2] # First request is repeated + actual = client.list_labels(98765) + expected = [models.Label.from_json(item) for item in full_response["labels"]] + assert mock.call_count == 3 + assert actual == expected + + +def test_for_an_error_when_total_size_doesnt_match_items(client, mocker, get_response): + full_response = get_response("list_labels") + page_1 = {"totalSize": 4, "labels": full_response["labels"][:3]} + page_2 = {"totalSize": 4, "labels": full_response["labels"][3:]} + mock = mocker.patch.object(client, "request_json") + mock.side_effect = [page_1, page_1, page_2] # First request is repeated + with pytest.raises(HTTPError): + client.list_labels(98765) + + +def test_that_launch_workflow_works(client, mocker, get_response): + mock = mocker.patch.object(client, "request_json") + mock.return_value = get_response("launch_workflow") + launch_spec = models.LaunchInfo( + compute_env_id="foo", + pipeline="bar", + work_dir="s3://path", + profiles=["test"], + ) + client.launch_workflow(launch_spec) + mock.assert_called_once() + + def test_that_get_workflow_returns_expected_response(client, mocker, get_response): expected = get_response("get_workflow") mock = mocker.patch.object(client, "get") diff --git a/tests/services/nextflowtower/test_enums.py b/tests/services/nextflowtower/test_enums.py index b556fa8..bece47f 100644 --- a/tests/services/nextflowtower/test_enums.py +++ b/tests/services/nextflowtower/test_enums.py @@ -1,4 +1,4 @@ -from orca.services.nextflowtower.models.enums import TaskStatus +from orca.services.nextflowtower.models import TaskStatus def test_that_TaskStatus_contant_values_are_correct(): diff --git a/tests/services/nextflowtower/test_integration.py b/tests/services/nextflowtower/test_integration.py index abd9b7e..79ebcb8 100644 --- a/tests/services/nextflowtower/test_integration.py +++ b/tests/services/nextflowtower/test_integration.py @@ -1,12 +1,50 @@ import pytest -from orca.services.nextflowtower import NextflowTowerClient, NextflowTowerConfig +from orca.services.nextflowtower import ( + NextflowTowerClient, + NextflowTowerConfig, + NextflowTowerOps, + models, +) + + +@pytest.fixture +def config(): + yield NextflowTowerConfig() + + +@pytest.fixture +def client(config): + yield NextflowTowerClient(config.auth_token, config.api_endpoint) + + +@pytest.fixture +def ops(config): + yield NextflowTowerOps(config) @pytest.mark.integration -def test_that_a_valid_client_can_be_constructed_and_tested(): - config = NextflowTowerConfig() +def test_that_the_config_can_pull_information_from_env(config): assert config.auth_token assert config.api_endpoint - client = NextflowTowerClient(config.auth_token, config.api_endpoint) + + +@pytest.mark.integration +def test_that_a_valid_client_can_be_constructed_and_tested(client): assert client.list_user_workspaces() + + +@pytest.mark.cost +@pytest.mark.integration +def test_that_a_workflow_can_be_launched(ops): + scratch_bucket = "s3://orca-service-test-project-tower-scratch/" + launch_info = models.LaunchInfo( + compute_env_id="5ykJFs33AE3d3AgThavz3b", + pipeline="nf-core/rnaseq", + revision="3.11.2", + profiles=["test"], + params={"outdir": f"{scratch_bucket}/2days/launch_test"}, + work_dir=f"{scratch_bucket}/work", + ) + workflow_id = ops.launch_workflow(launch_info, "ondemand") + assert workflow_id diff --git a/tests/services/nextflowtower/test_ops.py b/tests/services/nextflowtower/test_ops.py index 9eba021..4f8cd16 100644 --- a/tests/services/nextflowtower/test_ops.py +++ b/tests/services/nextflowtower/test_ops.py @@ -1,11 +1,14 @@ +from dataclasses import replace + import pytest from orca.errors import ConfigError -from orca.services.nextflowtower import NextflowTowerConfig, NextflowTowerOps +from orca.services.nextflowtower import NextflowTowerConfig, NextflowTowerOps, models +from orca.services.nextflowtower.utils import parse_datetime def test_that_the_workspace_attribute_is_accessible(ops): - assert ops.workspace == "Foo-Bar/project-2" + assert ops.workspace == "foo-bar/project-2" def test_for_an_error_when_the_workspace_attribute_is_missing(patch_os_environ): @@ -15,26 +18,126 @@ def test_for_an_error_when_the_workspace_attribute_is_missing(patch_os_environ): ops.workspace -def test_that_the_workspace_id_attribute_is_accessible(ops, mocker, get_response): - response = get_response("get_user_workspaces_and_orgs") - mock = mocker.patch.object(NextflowTowerOps, "client") - response = response["orgsAndWorkspaces"] - mock.list_user_workspaces.return_value = response - assert ops.workspace_id == 98765 +def test_that_the_workspace_id_attribute_is_accessible( + client, config, mocker, get_response +): + response = get_response("get_user_workspaces_and_orgs")["orgsAndWorkspaces"] + workspaces_json = [item for item in response if item["workspaceId"]] + workspaces = [models.Workspace.from_json(item) for item in workspaces_json] + mock = mocker.patch.object(NextflowTowerOps, "client", return_value=client) + mock.list_user_workspaces.return_value = workspaces -def test_for_error_when_the_workspace_id_does_not_exist(ops, mocker, get_response): - # Get rid of relevant entry - response = get_response("get_user_workspaces_and_orgs") - items = response["orgsAndWorkspaces"] - items_filtered = [item for item in items if item["workspaceName"] != "project-2"] + config.workspace = "foo-bar/project-1" + ops = NextflowTowerOps(config) + assert ops.workspace_id == 54321 - mock = mocker.patch.object(NextflowTowerOps, "client") - mock.list_user_workspaces.return_value = items_filtered + +def test_for_error_when_the_workspace_id_does_not_exist( + client, config, mocker, get_response +): + response = get_response("get_user_workspaces_and_orgs")["orgsAndWorkspaces"] + workspaces_json = [item for item in response if item["workspaceId"]] + workspaces = [models.Workspace.from_json(item) for item in workspaces_json] + + mock = mocker.patch.object(NextflowTowerOps, "client", return_value=client) + mock.list_user_workspaces.return_value = workspaces + + config.workspace = "foo-bar/project-3" + ops = NextflowTowerOps(config) with pytest.raises(ValueError): ops.workspace_id +def test_that_get_latest_compute_env_handles_multiple_envs(mocked_ops, get_response): + # Patch list_compute_envs() to return a list of ComputeEnvSummary objects + # This should result in two matching compute environments + response = get_response("list_compute_envs") + summaries = map(models.ComputeEnvSummary.from_json, response["computeEnvs"]) + mocked_ops.client.list_compute_envs.return_value = list(summaries) + + # Create function for generating compute environments + def generate_compute_env(id, date): + compute_env_response = get_response("get_compute_env")["computeEnv"] + compute_env = models.ComputeEnv.from_json(compute_env_response) + return replace(compute_env, id=id, date_created=parse_datetime(date)) + + # Generate full compute environments + mocked_ops.client.get_compute_env.side_effect = [ + generate_compute_env("5ykJF", "2023-04-26T00:49:49Z"), + generate_compute_env("9W2l7", "2023-01-11T13:32:54Z"), + ] + + result = mocked_ops.get_latest_compute_env("ondemand") + assert result == "5ykJF" + + +def test_that_get_latest_compute_env_handles_single_env(mocked_ops, get_response): + # Patch list_compute_envs() to return a list of ComputeEnvSummary objects + # This should result in one matching compute environment + response = get_response("list_compute_envs") + del response["computeEnvs"][2] + summaries = map(models.ComputeEnvSummary.from_json, response["computeEnvs"]) + mocked_ops.client.list_compute_envs.return_value = list(summaries) + + result = mocked_ops.get_latest_compute_env("ondemand") + assert result == "5ykJF" + + +def test_for_an_error_when_get_latest_compute_env_finds_no_env(mocked_ops): + mocked_ops.client.list_compute_envs.return_value = list() + with pytest.raises(ValueError): + mocked_ops.get_latest_compute_env() + + +def test_that_create_label_doesnt_create_duplicates(mocked_ops, get_response): + labels_json = get_response("list_labels")["labels"] + labels = [models.Label.from_json(item) for item in labels_json] + mocked_ops.client.list_labels.return_value = labels + + mocked_ops.create_label("launched-by-orca") + mocked_ops.client.create_label.assert_not_called() + + +def test_that_create_label_creates_missing_labels(mocked_ops, get_response): + labels_json = get_response("list_labels")["labels"] + labels = [models.Label.from_json(item) for item in labels_json] + mocked_ops.client.list_labels.return_value = labels + + mocked_ops.create_label("something-missing") + mocked_ops.client.create_label.assert_called_once() + + +def test_that_create_label_ignores_resource_labels(mocked_ops, get_response): + labels_json = get_response("list_labels")["labels"] + labels = [models.Label.from_json(item) for item in labels_json] + mocked_ops.client.list_labels.return_value = labels + + mocked_ops.create_label("CostCenter") # Existing resource label + mocked_ops.client.create_label.assert_called_once() + + +def test_that_launch_workflow_works(mocked_ops, get_response, mocker): + launch_info = models.LaunchInfo( + compute_env_id="5ykJF", + pipeline="some/pipeline", + revision="1.1", + profiles=["test"], + params={"outdir": "foo"}, + work_dir="bar", + ) + + mocker.patch.object(mocked_ops, "get_latest_compute_env") + + compute_env_response = get_response("get_compute_env")["computeEnv"] + compute_env = models.ComputeEnv.from_json(compute_env_response) + mocked_ops.client.get_compute_env.return_value = compute_env + + mocked_ops.launch_workflow(launch_info, "ondemand") + mocked_ops.client.launch_workflow.assert_called_once() + assert launch_info.compute_env_id == compute_env.id + + def test_that_get_workflow_status_returns_expected_tuple_workflow_is_complete( mocker, get_response, mocked_ops ): diff --git a/tests/services/nextflowtower/test_utils.py b/tests/services/nextflowtower/test_utils.py new file mode 100644 index 0000000..4c760c6 --- /dev/null +++ b/tests/services/nextflowtower/test_utils.py @@ -0,0 +1,8 @@ +from datetime import datetime, timezone + +from orca.services.nextflowtower import utils + + +def test_that_parse_datetime_works(): + result = utils.parse_datetime("2023-04-26T00:49:49Z") + assert result == datetime(2023, 4, 26, 0, 49, 49, tzinfo=timezone.utc) diff --git a/tox.ini b/tox.ini index f1b9844..448e310 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ extras = all commands = # The `-m ""` overrides the `-m "not slow"` in setup.cfg - pytest {posargs} -m "not acceptance" + pytest {posargs} -m "not acceptance and not cost" [testenv:lint]