diff --git a/abr-testing/Makefile b/abr-testing/Makefile index b9f92229177..5c5cc6d06df 100644 --- a/abr-testing/Makefile +++ b/abr-testing/Makefile @@ -95,7 +95,7 @@ abr-setup: .PHONY: simulate PROTOCOL_DIR := abr_testing/protocols -SIMULATION_TOOL := protocol_simulation/abr_sim_check.py +SIMULATION_TOOL := abr_testing/protocol_simulation/abr_sim_check.py EXTENSION := .py simulate: $(python) $(SIMULATION_TOOL) \ No newline at end of file diff --git a/abr-testing/Pipfile b/abr-testing/Pipfile index 0ea9e6f76aa..90534f708ae 100644 --- a/abr-testing/Pipfile +++ b/abr-testing/Pipfile @@ -18,7 +18,7 @@ slackclient = "*" slack-sdk = "*" pandas = "*" pandas-stubs = "*" -numpy = "==1.8.3" +paramiko = "*" [dev-packages] atomicwrites = "==1.4.1" diff --git a/abr-testing/Pipfile.lock b/abr-testing/Pipfile.lock index 08da1926e92..79885cdc940 100644 --- a/abr-testing/Pipfile.lock +++ b/abr-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f2a4a8a95be01ccb8425c8069a3dc8e3f932c1f9b36f5b12c838ee9cfc26015e" + "sha256": "a537d1f1a5f5d0658a3ba2c62deabf390fd7b9e72acbee6704f8d095c1b535e9" }, "pipfile-spec": 6, "requires": { @@ -22,108 +22,108 @@ }, "aiohappyeyeballs": { "hashes": [ - "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2", - "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd" + "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", + "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572" ], "markers": "python_version >= '3.8'", - "version": "==2.4.0" + "version": "==2.4.3" }, "aiohttp": { "hashes": [ - "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277", - "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1", - "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe", - "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb", - "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca", - "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91", - "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972", - "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a", - "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3", - "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa", - "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77", - "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b", - "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8", - "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599", - "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc", - "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf", - "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511", - "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699", - "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487", - "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987", - "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff", - "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db", - "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022", - "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce", - "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a", - "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5", - "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7", - "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820", - "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf", - "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e", - "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf", - "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5", - "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6", - "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6", - "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91", - "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3", - "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a", - "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d", - "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088", - "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc", - "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f", - "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75", - "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471", - "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e", - "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697", - "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092", - "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69", - "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3", - "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32", - "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589", - "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178", - "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92", - "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2", - "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e", - "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058", - "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857", - "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1", - "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6", - "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22", - "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0", - "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b", - "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57", - "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f", - "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e", - "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16", - "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1", - "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f", - "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6", - "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04", - "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae", - "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d", - "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b", - "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f", - "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862", - "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689", - "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c", - "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683", - "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef", - "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f", - "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12", - "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73", - "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061", - "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072", - "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11", - "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691", - "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77", - "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385", - "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172", - "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569", - "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f", - "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5" + "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", + "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c", + "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24", + "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480", + "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2", + "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5", + "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", + "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", + "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", + "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", + "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486", + "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", + "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", + "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", + "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68", + "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1", + "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d", + "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd", + "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", + "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8", + "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7", + "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", + "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7", + "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", + "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", + "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", + "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", + "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", + "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8", + "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", + "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", + "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", + "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", + "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce", + "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b", + "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8", + "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", + "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", + "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a", + "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", + "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", + "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab", + "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", + "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", + "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9", + "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572", + "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554", + "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d", + "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257", + "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", + "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b", + "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", + "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090", + "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6", + "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc", + "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", + "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", + "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", + "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", + "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", + "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", + "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026", + "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb", + "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", + "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", + "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", + "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f", + "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983", + "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", + "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", + "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa", + "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c", + "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2", + "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", + "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", + "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762", + "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a", + "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8", + "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", + "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", + "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc", + "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91", + "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23", + "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527", + "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", + "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", + "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7", + "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f", + "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a", + "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", + "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414" ], "markers": "python_version >= '3.8'", - "version": "==3.10.5" + "version": "==3.10.10" }, "aionotify": { "hashes": [ @@ -165,6 +165,39 @@ "markers": "python_version >= '3.7'", "version": "==24.2.0" }, + "bcrypt": { + "hashes": [ + "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", + "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", + "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", + "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", + "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", + "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170", + "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", + "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", + "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", + "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184", + "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", + "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", + "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", + "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", + "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", + "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", + "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", + "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", + "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", + "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", + "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", + "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", + "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", + "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", + "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", + "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", + "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" + ], + "markers": "python_version >= '3.7'", + "version": "==4.2.0" + }, "cachetools": { "hashes": [ "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", @@ -181,101 +214,189 @@ "markers": "python_version >= '3.6'", "version": "==2024.8.30" }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" + }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "click": { "hashes": [ @@ -293,6 +414,39 @@ "markers": "platform_system == 'Windows'", "version": "==0.4.6" }, + "cryptography": { + "hashes": [ + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" + ], + "markers": "python_version >= '3.7'", + "version": "==43.0.3" + }, "exceptiongroup": { "hashes": [ "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", @@ -303,111 +457,126 @@ }, "frozenlist": { "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", + "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", + "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", + "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", + "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", + "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", + "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", + "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", + "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", + "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", + "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", + "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", + "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c", + "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", + "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", + "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", + "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", + "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", + "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10", + "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", + "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", + "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", + "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", + "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10", + "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", + "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", + "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", + "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", + "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", + "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923", + "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", + "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", + "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", + "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", + "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", + "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", + "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", + "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", + "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", + "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", + "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", + "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", + "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", + "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", + "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", + "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604", + "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", + "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", + "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", + "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", + "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", + "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", + "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", + "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", + "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3", + "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", + "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", + "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", + "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf", + "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", + "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", + "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171", + "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", + "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", + "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", + "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", + "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", + "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", + "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9", + "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", + "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723", + "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", + "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", + "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99", + "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e", + "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", + "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", + "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", + "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", + "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", + "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca", + "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", + "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", + "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", + "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", + "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307", + "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e", + "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", + "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", + "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", + "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", + "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a" ], "markers": "python_version >= '3.8'", - "version": "==1.4.1" + "version": "==1.5.0" }, "google-api-core": { "hashes": [ - "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", - "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" + "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", + "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" ], "markers": "python_version >= '3.7'", - "version": "==2.19.2" + "version": "==2.21.0" }, "google-api-python-client": { "hashes": [ - "sha256:41f671be10fa077ee5143ee9f0903c14006d39dc644564f4e044ae96b380bf68", - "sha256:b1e62c9889c5ef6022f11d30d7ef23dc55100300f0e8aaf8aa09e8e92540acad" + "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", + "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.146.0" + "version": "==2.149.0" }, "google-auth": { "hashes": [ - "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", - "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc" + "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f", + "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a" ], "markers": "python_version >= '3.7'", - "version": "==2.34.0" + "version": "==2.35.0" }, "google-auth-httplib2": { "hashes": [ @@ -607,7 +776,6 @@ "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" ], - "index": "pypi", "markers": "python_version >= '3.9'", "version": "==1.26.4" }, @@ -657,48 +825,174 @@ }, "pandas": { "hashes": [ - "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863", - "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2", - "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", - "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", - "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", - "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", - "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51", - "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", - "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08", - "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", - "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4", - "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921", - "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", - "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", - "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0", - "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", - "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99", - "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", - "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd", - "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce", - "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57", - "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", - "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", - "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a", - "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238", - "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", - "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772", - "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", - "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad" + "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", + "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", + "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", + "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", + "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", + "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", + "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea", + "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", + "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", + "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", + "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", + "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", + "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", + "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e", + "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", + "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", + "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", + "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30", + "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", + "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", + "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", + "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", + "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", + "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", + "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", + "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761", + "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", + "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", + "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c", + "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c", + "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", + "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", + "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", + "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", + "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", + "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39", + "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", + "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", + "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", + "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", + "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", + "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.2.2" + "version": "==2.2.3" }, "pandas-stubs": { "hashes": [ - "sha256:3c0951a2c3e45e3475aed9d80b7147ae82f176b9e42e9fb321cfdebf3d411b3d", - "sha256:e230f5fa4065f9417804f4d65cd98f86c002efcc07933e8abcd48c3fad9c30a2" + "sha256:3a6f8f142105a42550be677ba741ba532621f4e0acad2155c0e7b2450f114cfa", + "sha256:d4ab618253f0acf78a5d0d2bfd6dffdd92d91a56a69bdc8144e5a5c6d25be3b5" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==2.2.2.240909" + "version": "==2.2.3.241009" + }, + "paramiko": { + "hashes": [ + "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9", + "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.5.0" + }, + "propcache": { + "hashes": [ + "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", + "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", + "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", + "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", + "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", + "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", + "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", + "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", + "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", + "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", + "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", + "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", + "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", + "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", + "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", + "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", + "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", + "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", + "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", + "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", + "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", + "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", + "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", + "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", + "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", + "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", + "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", + "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", + "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", + "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", + "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", + "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", + "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", + "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", + "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", + "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", + "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", + "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", + "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", + "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", + "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", + "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", + "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", + "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", + "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", + "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", + "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", + "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", + "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", + "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", + "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", + "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", + "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", + "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", + "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", + "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", + "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", + "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", + "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", + "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", + "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", + "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", + "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", + "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", + "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", + "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", + "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", + "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", + "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", + "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", + "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", + "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", + "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", + "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", + "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", + "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", + "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", + "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", + "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", + "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", + "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", + "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", + "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", + "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", + "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", + "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", + "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", + "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", + "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", + "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", + "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", + "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", + "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", + "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", + "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", + "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", + "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", + "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504" + ], + "markers": "python_version >= '3.8'", + "version": "==0.2.0" }, "proto-plus": { "hashes": [ @@ -710,20 +1004,20 @@ }, "protobuf": { "hashes": [ - "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132", - "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f", - "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece", - "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0", - "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f", - "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0", - "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276", - "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7", - "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3", - "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36", - "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d" + "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", + "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535", + "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b", + "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548", + "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", + "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", + "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36", + "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", + "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", + "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", + "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed" ], "markers": "python_version >= '3.8'", - "version": "==5.28.2" + "version": "==5.28.3" }, "pyasn1": { "hashes": [ @@ -741,6 +1035,14 @@ "markers": "python_version >= '3.8'", "version": "==0.4.1" }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, "pydantic": { "hashes": [ "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", @@ -790,13 +1092,29 @@ "markers": "python_version >= '3.7'", "version": "==1.10.18" }, + "pynacl": { + "hashes": [ + "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", + "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", + "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", + "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", + "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", + "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", + "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", + "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", + "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", + "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" + ], + "markers": "python_version >= '3.6'", + "version": "==1.5.0" + }, "pyparsing": { "hashes": [ - "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", - "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], "markers": "python_version >= '3.1'", - "version": "==3.1.4" + "version": "==3.2.0" }, "pyrsistent": { "hashes": [ @@ -876,23 +1194,27 @@ }, "pywin32": { "hashes": [ - "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", - "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65", - "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", - "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", - "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4", - "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", - "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", - "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36", - "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", - "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", - "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802", - "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a", - "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", - "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0" + "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", + "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", + "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6", + "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", + "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff", + "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de", + "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", + "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", + "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0", + "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", + "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", + "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920", + "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341", + "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", + "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", + "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", + "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", + "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4" ], "markers": "platform_system == 'Windows' and platform_python_implementation == 'CPython'", - "version": "==306" + "version": "==308" }, "requests": { "hashes": [ @@ -920,11 +1242,11 @@ }, "setuptools": { "hashes": [ - "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", - "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" + "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec", + "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8" ], "markers": "python_version >= '3.8'", - "version": "==75.1.0" + "version": "==75.2.0" }, "six": { "hashes": [ @@ -936,12 +1258,12 @@ }, "slack-sdk": { "hashes": [ - "sha256:070eb1fb355c149a5f80fa0be6eeb5f5588e4ddff4dd76acf060454435cb037e", - "sha256:853bb55154115d080cae342c4099f2ccb559a78ae8d0f5109b49842401a920fa" + "sha256:e328bb661d95db5f66b993b1d64288ac7c72201a745b4c7cf8848dafb7b74e40", + "sha256:ef93beec3ce9c8f64da02fd487598a05ec4bc9c92ceed58f122dbe632691cbe2" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.33.0" + "version": "==3.33.1" }, "slackclient": { "hashes": [ @@ -978,11 +1300,11 @@ }, "types-pytz": { "hashes": [ - "sha256:4433b5df4a6fc587bbed41716d86a5ba5d832b4378e506f40d34bc9c81df2c24", - "sha256:a1eebf57ebc6e127a99d2fa2ba0a88d2b173784ef9b3defcc2004ab6855a44df" + "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7", + "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44" ], "markers": "python_version >= '3.8'", - "version": "==2024.2.0.20240913" + "version": "==2024.2.0.20241003" }, "typing-extensions": { "hashes": [ @@ -994,11 +1316,11 @@ }, "tzdata": { "hashes": [ - "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", - "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", + "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" ], "markers": "python_version >= '2'", - "version": "==2024.1" + "version": "==2024.2" }, "uritemplate": { "hashes": [ @@ -1094,101 +1416,91 @@ }, "yarl": { "hashes": [ - "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", - "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", - "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", - "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", - "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", - "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", - "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", - "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", - "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", - "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", - "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", - "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", - "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", - "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", - "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", - "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", - "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", - "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", - "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", - "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", - "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", - "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", - "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", - "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", - "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", - "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", - "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", - "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", - "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", - "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", - "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", - "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", - "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", - "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", - "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", - "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", - "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", - "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", - "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", - "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", - "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", - "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", - "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", - "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", - "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", - "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", - "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", - "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", - "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", - "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", - "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", - "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", - "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", - "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", - "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", - "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", - "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", - "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", - "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", - "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", - "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", - "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", - "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", - "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", - "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", - "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", - "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", - "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", - "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", - "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", - "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", - "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", - "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", - "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", - "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", - "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", - "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", - "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", - "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", - "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", - "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", - "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", - "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", - "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", - "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", - "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", - "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", - "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", - "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", - "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", - "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", - "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" + "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9", + "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36", + "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240", + "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2", + "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581", + "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929", + "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3", + "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6", + "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552", + "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472", + "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2", + "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb", + "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7", + "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b", + "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b", + "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058", + "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a", + "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656", + "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71", + "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3", + "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837", + "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6", + "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0", + "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104", + "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca", + "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb", + "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7", + "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07", + "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b", + "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202", + "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d", + "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532", + "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f", + "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5", + "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3", + "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724", + "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2", + "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09", + "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732", + "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2", + "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120", + "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4", + "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027", + "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e", + "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d", + "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b", + "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16", + "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120", + "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5", + "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97", + "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84", + "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00", + "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596", + "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d", + "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56", + "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7", + "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283", + "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67", + "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c", + "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968", + "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916", + "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae", + "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8", + "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604", + "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4", + "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af", + "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f", + "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a", + "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428", + "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9", + "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b", + "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059", + "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3", + "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49", + "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3", + "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade", + "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3", + "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c", + "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7", + "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349", + "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243", + "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7" ], - "markers": "python_version >= '3.8'", - "version": "==1.11.1" + "markers": "python_version >= '3.9'", + "version": "==1.16.0" } }, "develop": { @@ -1255,99 +1567,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "click": { "hashes": [ @@ -1367,81 +1694,71 @@ }, "coverage": { "hashes": [ - "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", - "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", - "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", - "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", - "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", - "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", - "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", - "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", - "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", - "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", - "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", - "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", - "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", - "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", - "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", - "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", - "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", - "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", - "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", - "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", - "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", - "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", - "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", - "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", - "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", - "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", - "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", - "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", - "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", - "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", - "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", - "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", - "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", - "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", - "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", - "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", - "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", - "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", - "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", - "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", - "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", - "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", - "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", - "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", - "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", - "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", - "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", - "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", - "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", - "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", - "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", - "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", - "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", - "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", - "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", - "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", - "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", - "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", - "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", - "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", - "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", - "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", - "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", - "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", - "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", - "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", - "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", - "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", - "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", - "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", - "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", - "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" + "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376", + "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", + "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111", + "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", + "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", + "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", + "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", + "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", + "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", + "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c", + "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", + "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", + "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", + "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0", + "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", + "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", + "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", + "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", + "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", + "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", + "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", + "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", + "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", + "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", + "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", + "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", + "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", + "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", + "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", + "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901", + "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", + "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", + "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", + "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", + "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", + "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", + "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", + "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", + "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3", + "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", + "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076", + "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", + "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", + "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", + "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", + "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", + "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", + "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09", + "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", + "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", + "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f", + "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", + "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", + "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", + "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", + "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", + "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", + "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", + "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", + "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", + "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", + "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858" ], - "markers": "python_version >= '3.8'", - "version": "==7.6.1" + "markers": "python_version >= '3.9'", + "version": "==7.6.4" }, "flake8": { "hashes": [ @@ -1480,37 +1797,37 @@ }, "google-api-core": { "hashes": [ - "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", - "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" + "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", + "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" ], "markers": "python_version >= '3.7'", - "version": "==2.19.2" + "version": "==2.21.0" }, "google-api-python-client": { "hashes": [ - "sha256:41f671be10fa077ee5143ee9f0903c14006d39dc644564f4e044ae96b380bf68", - "sha256:b1e62c9889c5ef6022f11d30d7ef23dc55100300f0e8aaf8aa09e8e92540acad" + "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", + "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.146.0" + "version": "==2.149.0" }, "google-api-python-client-stubs": { "hashes": [ - "sha256:148e16613e070969727f39691e23a73cdb87c65a4fc8133abd4c41d17b80b313", - "sha256:3c1f9f2a7cac8d1e9a7e84ed24e6c29cf4c643b0f94e39ed09ac1b7e91ab239a" + "sha256:7327c058fb5ba975309922f962f17931b9c82af51d95a5dc04061ed0c20b9f06", + "sha256:75b3dfe67b9d74ac3b58d78725326836769d0b2df1cbef354a5455a5cc57d68d" ], "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.27.0" + "markers": "python_version >= '3.7'", + "version": "==1.28.0" }, "google-auth": { "hashes": [ - "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", - "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc" + "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f", + "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a" ], "markers": "python_version >= '3.7'", - "version": "==2.34.0" + "version": "==2.35.0" }, "google-auth-httplib2": { "hashes": [ @@ -1643,20 +1960,20 @@ }, "protobuf": { "hashes": [ - "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132", - "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f", - "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece", - "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0", - "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f", - "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0", - "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276", - "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7", - "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3", - "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36", - "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d" + "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", + "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535", + "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b", + "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548", + "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", + "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", + "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36", + "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", + "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", + "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", + "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed" ], "markers": "python_version >= '3.8'", - "version": "==5.28.2" + "version": "==5.28.3" }, "py": { "hashes": [ @@ -1708,11 +2025,11 @@ }, "pyparsing": { "hashes": [ - "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", - "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], "markers": "python_version >= '3.1'", - "version": "==3.1.4" + "version": "==3.2.0" }, "pytest": { "hashes": [ @@ -1757,11 +2074,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "types-httplib2": { "hashes": [ diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py index f25c89d8435..1440dfd70a8 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -5,6 +5,7 @@ import json import sys import traceback +import hashlib from abr_testing.data_collection import read_robot_logs from abr_testing.automation import google_drive_tool, google_sheets_tool @@ -75,10 +76,6 @@ def module_helper( for module in range(len(modules)): one_module = modules[module] mod_serial = one_module["serialNumber"] - modified = "No data" - x = "" - y = "" - z = "" try: modified = one_module["moduleOffset"].get("last_modified", "") x = one_module["moduleOffset"]["offset"].get("x", "") @@ -97,13 +94,21 @@ def module_helper( return modules_upload_rows +def create_hash( + robot_name: str, deck_slot: str, pipette_calibrated_with: str, last_modified: str +) -> str: + """Create unique hash identifier for deck calibrations.""" + combined_string = robot_name + deck_slot + pipette_calibrated_with + last_modified + hashed_obj = hashlib.sha256(combined_string.encode()) + return hashed_obj.hexdigest() + + def deck_helper( headers_beg: List[str], headers_end: List[str], calibration_log: Dict[Any, Any], google_sheet_name: str, - deck_sheet_serials: Set[str], - deck_sheet_modify_dates: Set[str], + deck_sheet_hashes: Set[str], storage_directory: str, ) -> List[Any]: """Helper for parsing deck calibration data.""" @@ -117,11 +122,16 @@ def deck_helper( ) # DECK DATA deck = calibration_log["Deck"] - deck_modified = deck["data"].get("lastModified", "") + deck_modified = str(deck["data"].get("lastModified")) slots = ["D3", "D1", "A1"] - pipette_calibrated_with = deck["data"].get("pipetteCalibratedWith", "") + pipette_calibrated_with = str(deck["data"].get("pipetteCalibratedWith", "")) for i in range(len(deck["data"]["matrix"])): - if slots[i] in deck_sheet_serials and deck_modified in deck_sheet_modify_dates: + robot = calibration_log["Robot"] + deck_slot = slots[i] + unique_hash = create_hash( + robot, deck_slot, pipette_calibrated_with, deck_modified + ) + if unique_hash in deck_sheet_hashes: continue coords = deck["data"]["matrix"][i] x = coords[0] @@ -143,7 +153,7 @@ def send_batch_update( google_sheet_deck: google_sheets_tool.google_sheet, ) -> None: """Executes batch updates.""" - # Prepare for batch updates + # Prepare data for batch update try: transposed_instruments_upload_rows = list( map(list, zip(*instruments_upload_rows)) @@ -197,22 +207,19 @@ def upload_calibration_offsets( inst_sheet_serials: Set[str] = set() inst_sheet_modify_dates: Set[str] = set() module_sheet_serials: Set[str] = set() - deck_sheet_serials: Set[str] = set() - deck_sheet_modify_dates: Set[str] = set() - + deck_sheet_hashes: Set[str] = set() # Get current serials, and modified info from google sheet for i, sheet in enumerate(sheets): if i == 0: inst_sheet_serials = sheet.get_column(8) inst_sheet_modify_dates = sheet.get_column(15) if i == 1: - module_sheet_serials = sheet.get_column(8) + module_sheet_serials = sheet.get_column(6) module_modify_dates = sheet.get_column(15) elif i == 2: - deck_sheet_serials = sheet.get_column(6) - deck_sheet_modify_dates = sheet.get_column(10) + deck_sheet_hashes = sheet.get_column(11) - # Go through caliration logs and deterine what should be added to the sheet + # Iterate through calibration logs and accumulate data for calibration_log in calibration_data: for sheet_ind, sheet in enumerate(sheets): if sheet_ind == 0: @@ -241,8 +248,7 @@ def upload_calibration_offsets( headers_end, calibration_log, google_sheet_name, - deck_sheet_serials, - deck_sheet_modify_dates, + deck_sheet_hashes, storage_directory, ) send_batch_update( @@ -294,7 +300,6 @@ def run( ip, storage_directory ) calibration_data.append(calibration) - # upload_calibration_offsets(calibration, storage_directory) else: try: ( @@ -316,7 +321,7 @@ def run( google_sheet_deck, google_sheet_name, ) - print("Successfully uploaded callibration data!") + print("Successfully uploaded calibration data!") except Exception: print("No calibration data to upload: ") traceback.print_exc() diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index ff650335d84..d740518c7ac 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -694,7 +694,7 @@ def get_calibration_offsets( print(f"Connected to {ip}") except Exception: print(f"ERROR: Failed to read IP address: {ip}") - raise + pass health_data = response.json() robot_name = health_data.get("name", "") api_version = health_data.get("api_version", "") diff --git a/abr-testing/abr_testing/protocol_simulation/__init__.py b/abr-testing/abr_testing/protocol_simulation/__init__.py new file mode 100644 index 00000000000..d3776c77fad --- /dev/null +++ b/abr-testing/abr_testing/protocol_simulation/__init__.py @@ -0,0 +1 @@ +"""The package holding code for simulating protocols.""" diff --git a/abr-testing/protocol_simulation/abr_sim_check.py b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py similarity index 62% rename from abr-testing/protocol_simulation/abr_sim_check.py rename to abr-testing/abr_testing/protocol_simulation/abr_sim_check.py index a97a0b3692e..513860baa9b 100644 --- a/abr-testing/protocol_simulation/abr_sim_check.py +++ b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py @@ -1,26 +1,27 @@ -from protocol_simulation import simulation_metrics +"""Check ABR Protocols Simulate Successfully.""" +from abr_testing.protocol_simulation import simulation_metrics import os import traceback from pathlib import Path -def run(file_to_simulate: Path): - protocol_name = file_to_simulate.stem + +def run(file_to_simulate: str) -> None: + """Simulate protocol and raise errors.""" + protocol_name = Path(file_to_simulate).stem try: simulation_metrics.main(file_to_simulate, False) - except Exception as e: + except Exception: print(f"Error in protocol: {protocol_name}") traceback.print_exc() - - if __name__ == "__main__": # Directory to search - root_dir = 'abr_testing/protocols' + root_dir = "abr_testing/protocols" exclude = [ - '__init__.py', - 'shared_vars_and_funcs.py', + "__init__.py", + "shared_vars_and_funcs.py", ] # Walk through the root directory and its subdirectories for root, dirs, files in os.walk(root_dir): @@ -30,4 +31,4 @@ def run(file_to_simulate: Path): continue file_path = os.path.join(root, file) print(f"Simulating protocol: {file_path}") - run(Path(file_path)) \ No newline at end of file + run(file_path) diff --git a/abr-testing/protocol_simulation/simulation_metrics.py b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py similarity index 61% rename from abr-testing/protocol_simulation/simulation_metrics.py rename to abr-testing/abr_testing/protocol_simulation/simulation_metrics.py index dfbba90949b..418c1e1aacd 100644 --- a/abr-testing/protocol_simulation/simulation_metrics.py +++ b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py @@ -1,4 +1,4 @@ -import re +"""Creates google sheet to display metrics of protocol.""" import sys import os from pathlib import Path @@ -9,33 +9,38 @@ from datetime import datetime from abr_testing.automation import google_sheets_tool from abr_testing.data_collection import read_robot_logs -from typing import Set, Dict, Any, Tuple, List, Union +from typing import Any, Tuple, List, Dict, Union, NoReturn from abr_testing.tools import plate_reader - def set_api_level(protocol_file_path: str) -> None: + """Set API level for analysis.""" with open(protocol_file_path, "r") as file: file_contents = file.readlines() # Look for current'apiLevel:' for i, line in enumerate(file_contents): print(line) - if 'apiLevel' in line: + if "apiLevel" in line: print(f"The current API level of this protocol is: {line}") - change = input("Would you like to simulate with a different API level? (Y/N) ").strip().upper() + change = ( + input("Would you like to simulate with a different API level? (Y/N) ") + .strip() + .upper() + ) if change == "Y": api_level = input("Protocol API Level to Simulate with: ") # Update new API level - file_contents[i] = f'apiLevel: {api_level}\n' + file_contents[i] = f"apiLevel: {api_level}\n" print(f"Updated line: {file_contents[i]}") break with open(protocol_file_path, "w") as file: file.writelines(file_contents) print("File updated successfully.") + def look_for_air_gaps(protocol_file_path: str) -> int: - """Search Protocol for Air Gaps""" + """Search Protocol for Air Gaps.""" instances = 0 try: with open(protocol_file_path, "r") as open_file: @@ -44,63 +49,165 @@ def look_for_air_gaps(protocol_file_path: str) -> int: if "air_gap" in line: print(line) instances += 1 - print(f'Found {instances} instance(s) of the air gap function') + print(f"Found {instances} instance(s) of the air gap function") open_file.close() except Exception as error: - print("Error reading protocol:", error.with_traceback()) + print("Error reading protocol:", error) + raise error.with_traceback(error.__traceback__) return instances # Mock sys.exit to avoid program termination original_exit = sys.exit # Save the original sys.exit function -def mock_exit(code: Any = None) -> None: - """Prevents program from exiting after analyze""" + +def mock_exit(code: Union[str, int, None] = None) -> NoReturn: + """Prevents program from exiting after analysis.""" print(f"sys.exit() called with code: {code}") raise SystemExit(code) # Raise the exception but catch it to prevent termination + def get_labware_name(id: str, object_dict: dict, json_data: dict) -> str: - """Recursively find the labware_name""" + """Recursively find the labware_name.""" slot = "" for obj in object_dict: - if obj['id'] == id: + if obj["id"] == id: try: # Try to get the slotName from the location - slot = obj['location']['slotName'] + slot = obj["location"]["slotName"] return " SLOT: " + slot except KeyError: # Handle KeyError when location or slotName is missing - location = obj.get('location', {}) - + location = obj.get("location", {}) + # Check if location contains 'moduleId' - if 'moduleId' in location: - return get_labware_name(location['moduleId'], json_data['modules'], json_data) - + if "moduleId" in location: + return get_labware_name( + location["moduleId"], json_data["modules"], json_data + ) + # Check if location contains 'labwareId' - elif 'labwareId' in location: - return get_labware_name(location['labwareId'], json_data['labware'], json_data) - + elif "labwareId" in location: + return get_labware_name( + location["labwareId"], json_data["labware"], json_data + ) + return " Labware not found" -def parse_results_volume(json_data_file: str) -> Tuple[ - List[str], List[str], List[str], List[str], - List[str], List[str], List[str], List[str], - List[str], List[str], List[str] - ]: - """Pars run log and extract neccessay information""" - json_data = [] +def determine_liquid_movement_volumes( + commands: List[Dict[str, Any]], json_data: Dict[str, Any] +) -> Dict[str, Any]: + """Determine where liquid is moved during protocol.""" + labware_well_dict: Dict[str, Any] = {} + for x, command in enumerate(commands): + if x != 0: + if command["commandType"] == "aspirate": + labware_id = command["params"]["labwareId"] + labware_name = "" + for labware in json_data.get("labware", {}): + if labware["id"] == labware_id: + labware_name = (labware["loadName"]) + get_labware_name( + labware["id"], json_data["labware"], json_data + ) + well_name = command["params"]["wellName"] + + if labware_id not in labware_well_dict: + labware_well_dict[labware_id] = {} + + if well_name not in labware_well_dict[labware_id]: + labware_well_dict[labware_id][well_name] = (labware_name, 0, 0, "") + + vol = int(command["params"]["volume"]) + + ( + labware_name, + added_volumes, + subtracted_volumes, + log, + ) = labware_well_dict[labware_id][well_name] + + subtracted_volumes += vol + log += f"aspirated {vol} " + labware_well_dict[labware_id][well_name] = ( + labware_name, + added_volumes, + subtracted_volumes, + log, + ) + + elif command["commandType"] == "dispense": + labware_id = command["params"]["labwareId"] + labware_name = "" + for labware in json_data.get("labware", {}): + if labware["id"] == labware_id: + labware_name = (labware["loadName"]) + get_labware_name( + labware["id"], json_data["labware"], json_data + ) + well_name = command["params"]["wellName"] + + if labware_id not in labware_well_dict: + labware_well_dict[labware_id] = {} + + if well_name not in labware_well_dict[labware_id]: + labware_well_dict[labware_id][well_name] = (labware_name, 0, 0, "") + + vol = int(command["params"]["volume"]) + + ( + labware_name, + added_volumes, + subtracted_volumes, + log, + ) = labware_well_dict[labware_id][well_name] + + added_volumes += vol + log += f"dispensed {vol} " + labware_well_dict[labware_id][well_name] = ( + labware_name, + added_volumes, + subtracted_volumes, + log, + ) + return labware_well_dict + + +def parse_results_volume( + json_data_file: str, + protocol_name: str, + file_date: datetime, + file_date_formatted: str, + hellma_plate_standards: List[Any], +) -> Tuple[ + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], +]: + """Parse run log and extract necessary information.""" + json_data = {} with open(json_data_file, "r") as json_file: json_data = json.load(json_file) - commands = json_data.get("commands", []) + if isinstance(json_data, dict): + commands = json_data.get("commands", {}) + else: + print(f"Expected JSON object (dict) but got {type(json_data).__name__}.") + commands = {} start_time = datetime.fromisoformat(commands[0]["createdAt"]) - end_time = datetime.fromisoformat(commands[len(commands)-1]["completedAt"]) + end_time = datetime.fromisoformat(commands[len(commands) - 1]["completedAt"]) header = ["", "Protocol Name", "Date", "Time"] header_fill_row = ["", protocol_name, str(file_date.date()), str(file_date.time())] - labware_names_row =["Labware Name"] - volume_dispensed_row =["Total Volume Dispensed uL"] - volume_aspirated_row =["Total Volume Aspirated uL"] + labware_names_row = ["Labware Name"] + volume_dispensed_row = ["Total Volume Dispensed uL"] + volume_aspirated_row = ["Total Volume Aspirated uL"] change_in_volume_row = ["Total Change in Volume uL"] start_time_row = ["Start Time"] end_time_row = ["End Time"] @@ -136,79 +243,54 @@ def parse_results_volume(json_data_file: str) -> Tuple[ "Gripper Pick Ups", "Total Liquid Probes", "Average Liquid Probe Time (sec)", - ] + ] values_row = ["Value"] - - labware_well_dict = {} - hs_dict, temp_module_dict, thermo_cycler_dict, plate_reader_dict, instrument_dict = {}, {}, {}, {}, {} + ( + hs_dict, + temp_module_dict, + thermo_cycler_dict, + plate_reader_dict, + instrument_dict, + ) = ({}, {}, {}, {}, {}) try: hs_dict = read_robot_logs.hs_commands(json_data) temp_module_dict = read_robot_logs.temperature_module_commands(json_data) thermo_cycler_dict = read_robot_logs.thermocycler_commands(json_data) - plate_reader_dict = read_robot_logs.plate_reader_commands(json_data, hellma_plate_standards) - instrument_dict = read_robot_logs.instrument_commands(json_data) - except: + plate_reader_dict = read_robot_logs.plate_reader_commands( + json_data, hellma_plate_standards + ) + instrument_dict = read_robot_logs.instrument_commands( + json_data, labware_name=None + ) + except KeyError: pass - metrics = [hs_dict, temp_module_dict, thermo_cycler_dict, plate_reader_dict, instrument_dict] - - for x, command in enumerate(commands): - if x != 0: - prev_command = commands[x-1] - if command["commandType"] == "aspirate": - labware_id = command["params"]["labwareId"] - labware_name = "" - for labware in json_data.get("labware"): - if labware["id"] == labware_id: - labware_name = (labware["loadName"]) + get_labware_name(labware["id"], json_data["labware"], json_data) - well_name = command["params"]["wellName"] - - if labware_id not in labware_well_dict: - labware_well_dict[labware_id] = {} - - if well_name not in labware_well_dict[labware_id]: - labware_well_dict[labware_id][well_name] = (labware_name, 0, 0, "") - - vol = int(command["params"]["volume"]) - - labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[labware_id][well_name] - - subtracted_volumes += vol - log+=(f"aspirated {vol} ") - labware_well_dict[labware_id][well_name] = (labware_name, added_volumes, subtracted_volumes, log) - - elif command["commandType"] == "dispense": - labware_id = command["params"]["labwareId"] - labware_name = "" - for labware in json_data.get("labware"): - if labware["id"] == labware_id: - labware_name = (labware["loadName"]) + get_labware_name(labware["id"], json_data["labware"], json_data) - well_name = command["params"]["wellName"] - - if labware_id not in labware_well_dict: - labware_well_dict[labware_id] = {} - - if well_name not in labware_well_dict[labware_id]: - labware_well_dict[labware_id][well_name] = (labware_name, 0, 0, "") - - vol = int(command["params"]["volume"]) - - labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[labware_id][well_name] - - added_volumes += vol - log+=(f"dispensed {vol} ") - labware_well_dict[labware_id][well_name] = (labware_name, added_volumes, subtracted_volumes, log) - with open(f"{os.path.dirname(json_data_file)}\\{protocol_name}_well_volumes_{file_date_formatted}.json", "w") as output_file: + metrics = [ + hs_dict, + temp_module_dict, + thermo_cycler_dict, + plate_reader_dict, + instrument_dict, + ] + # Determine liquid moved to and from labware + labware_well_dict = determine_liquid_movement_volumes(commands, json_data) + file_name_to_open = f"{protocol_name}_well_volumes_{file_date_formatted}.json" + with open( + f"{os.path.dirname(json_data_file)}\\{file_name_to_open}", + "w", + ) as output_file: json.dump(labware_well_dict, output_file) output_file.close() - + # populate row lists for labware_id in labware_well_dict.keys(): volume_added = 0 volume_subtracted = 0 - labware_name ="" + labware_name = "" for well in labware_well_dict[labware_id].keys(): - labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[labware_id][well] + labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[ + labware_id + ][well] volume_added += added_volumes volume_subtracted += subtracted_volumes labware_names_row.append(labware_name) @@ -222,31 +304,34 @@ def parse_results_volume(json_data_file: str) -> Tuple[ for metric in metrics: for cmd in metric.keys(): values_row.append(str(metric[cmd])) - return( - header, - header_fill_row, + return ( + header, + header_fill_row, labware_names_row, - volume_dispensed_row, - volume_aspirated_row, - change_in_volume_row, - start_time_row, - end_time_row, - total_time_row, - metrics_row, - values_row) - - -def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdir, google_sheet_name: str = "") -> None: - """Main module control""" + volume_dispensed_row, + volume_aspirated_row, + change_in_volume_row, + start_time_row, + end_time_row, + total_time_row, + metrics_row, + values_row, + ) + + +def main( + protocol_file_path_name: str, + save: bool, + storage_directory: str = os.curdir, + google_sheet_name: str = "", +) -> None: + """Main module control.""" sys.exit = mock_exit # Replace sys.exit with the mock function # Read file path from arguments - protocol_file_path = Path(protocol_file_path) - global protocol_name + protocol_file_path = Path(protocol_file_path_name) protocol_name = protocol_file_path.stem print("Simulating", protocol_name) - global file_date file_date = datetime.now() - global file_date_formatted file_date_formatted = file_date.strftime("%Y-%m-%d_%H-%M-%S") error_output = f"{storage_directory}\\test_debug" # Run protocol simulation @@ -254,7 +339,9 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi with Context(analyze) as ctx: if save: # Prepare output file - json_file_path = f"{storage_directory}\\{protocol_name}_{file_date_formatted}.json" + json_file_path = ( + f"{storage_directory}\\{protocol_name}_{file_date_formatted}.json" + ) json_file_output = open(json_file_path, "wb+") # log_output_file = f"{protocol_name}_log" ctx.invoke( @@ -264,7 +351,7 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi human_json_output=None, log_output=error_output, log_level="ERROR", - check=False + check=False, ) json_file_output.close() else: @@ -275,9 +362,9 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi human_json_output=None, log_output=error_output, log_level="ERROR", - check=True + check=True, ) - + except SystemExit as e: print(f"SystemExit caught with code: {e}") finally: @@ -286,38 +373,40 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi with open(error_output, "r") as open_file: try: errors = open_file.readlines() - if not errors: pass + if not errors: + pass else: print(errors) sys.exit(1) - except: + except FileNotFoundError: print("error simulating ...") sys.exit() if save: try: credentials_path = os.path.join(storage_directory, "credentials.json") print(credentials_path) - except FileNotFoundError: print(f"Add credentials.json file to: {storage_directory}.") sys.exit() - - global hellma_plate_standards - try: - hellma_plate_standards = plate_reader.read_hellma_plate_files(storage_directory, 101934) - - except: - print(f"Add helma plate standard files to {storage_directory}.") - sys.exit() + hellma_plate_standards = plate_reader.read_hellma_plate_files( + storage_directory, 101934 + ) google_sheet = google_sheets_tool.google_sheet( credentials_path, google_sheet_name, 0 ) google_sheet.write_to_row([]) - for row in parse_results_volume(json_file_path): + for row in parse_results_volume( + json_file_path, + protocol_name, + file_date, + file_date_formatted, + hellma_plate_standards, + ): print("Writing results to", google_sheet_name) print(str(row)) google_sheet.write_to_row(row) + if __name__ == "__main__": CLEAN_PROTOCOL = True parser = argparse.ArgumentParser(description="Read run logs on google drive.") @@ -330,7 +419,7 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi ) parser.add_argument( "sheet_name", - metavar="SHEETNAME", + metavar="SHEET_NAME", type=str, nargs=1, help="Name of sheet to upload results to", @@ -340,22 +429,24 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi metavar="PROTOCOL_FILE_PATH", type=str, nargs=1, - help="Path to protocol file" - + help="Path to protocol file", ) args = parser.parse_args() storage_directory = args.storage_directory[0] sheet_name = args.sheet_name[0] - protocol_file_path = args.protocol_file_path[0] - + protocol_file_path: str = args.protocol_file_path[0] SETUP = True - while(SETUP): - print("This current version cannot properly handle air gap calls.\nThese may cause simulation results to be inaccurate") + while SETUP: + print( + "Current version cannot handle air gap calls. Simulation results may be inaccurate." + ) air_gaps = look_for_air_gaps(protocol_file_path) if air_gaps > 0: choice = "" while not choice: - choice = input("This protocol contains air gaps, results may be innacurate, would you like to continue? (Y/N): ") + choice = input( + "Remove air_gap commands to ensure accurate results? (Y/N): " + ) if choice.upper() == "Y": SETUP = False CLEAN_PROTOCOL = True @@ -367,13 +458,15 @@ def main(protocol_file_path: Path, save: bool, storage_directory: str = os.curdi choice = "" print("Please enter a valid response.") SETUP = False - - # set_api_level() + + # Change api level if CLEAN_PROTOCOL: - set_api_level(Path(protocol_file_path)) + set_api_level(protocol_file_path) main( protocol_file_path, True, storage_directory, - sheet_name,) - else: sys.exit(0) \ No newline at end of file + sheet_name, + ) + else: + sys.exit(0) diff --git a/abr-testing/abr_testing/tools/abr_setup.py b/abr-testing/abr_testing/tools/abr_setup.py index 853f1c53ced..67ed5bfb333 100644 --- a/abr-testing/abr_testing/tools/abr_setup.py +++ b/abr-testing/abr_testing/tools/abr_setup.py @@ -10,6 +10,14 @@ abr_google_drive, abr_calibration_logs, ) +from abr_testing.tools import sync_abr_sheet + + +def run_sync_abr_sheet( + storage_directory: str, abr_data_sheet: str, room_conditions_sheet: str +) -> None: + """Sync ABR sheet with temp and lifetime percents.""" + sync_abr_sheet.run(storage_directory, abr_data_sheet, room_conditions_sheet) def run_temp_sensor(ip_file: str) -> None: @@ -50,7 +58,7 @@ def get_calibration_data( storage_directory, folder_name, google_sheet_name, email ) except Exception as e: - print("Cannot get callibration data", e) + print("Cannot get calibration data", e) traceback.print_exc() @@ -61,6 +69,8 @@ def main(configurations: configparser.ConfigParser) -> None: email = None drive_folder = None sheet_name = None + ambient_conditions_sheet = None + sheet_url = None has_defaults = False # If default is not specified get all values @@ -73,12 +83,14 @@ def main(configurations: configparser.ConfigParser) -> None: email = default["Email"] drive_folder = default["Drive_Folder"] sheet_name = default["Sheet_Name"] + sheet_url = default["Sheet_Url"] except KeyError as e: print("Cannot read config file\n" + str(e)) # Run Temperature Sensors if not has_defaults: ip_file = configurations["TEMP-SENSOR"]["Robo_List"] + ambient_conditions_sheet = configurations["TEMP-SENSOR"]["Sheet_Url"] print("Starting temp sensors...") if ip_file: run_temp_sensor(ip_file) @@ -92,6 +104,7 @@ def main(configurations: configparser.ConfigParser) -> None: email = configurations["RUN-LOG"]["Email"] drive_folder = configurations["RUN-LOG"]["Drive_Folder"] sheet_name = configurations["RUN-LOG"]["Sheet_Name"] + sheet_url = configurations["RUN-LOG"]["Sheet_Url"] print(sheet_name) if storage_directory and drive_folder and sheet_name and email: print("Retrieving robot run logs...") @@ -102,7 +115,9 @@ def main(configurations: configparser.ConfigParser) -> None: else: print("Storage, Email, or Drive Folder is missing, please fix configs") sys.exit(1) - + # Update Google Sheet with missing temp/rh + if storage_directory and sheet_url and ambient_conditions_sheet: + run_sync_abr_sheet(storage_directory, sheet_url, ambient_conditions_sheet) # Collect calibration data if not has_defaults: storage_directory = configurations["CALIBRATION"]["Storage"] diff --git a/abr-testing/abr_testing/tools/sync_abr_sheet.py b/abr-testing/abr_testing/tools/sync_abr_sheet.py index 569f0f9b834..2ae0769dec1 100644 --- a/abr-testing/abr_testing/tools/sync_abr_sheet.py +++ b/abr-testing/abr_testing/tools/sync_abr_sheet.py @@ -202,6 +202,30 @@ def connect_and_download( return file_paths, credentials_path +def run( + storage_directory: str, abr_data_sheet_url: str, abr_room_conditions_sheet: str +) -> None: + """Connect to storage and google sheets and update.""" + google_sheets_to_download = { + "ABR-run-data": abr_data_sheet_url, + "ABR Ambient Conditions": abr_room_conditions_sheet, + } + # Download google sheets. + + file_paths, credentials_path = connect_and_download( + google_sheets_to_download, storage_directory + ) + # Read csvs. + abr_data = read_csv_as_dict(file_paths[0]) + temp_data = read_csv_as_dict(file_paths[1]) + # Compare robot and timestamps. + abr_google_sheet = google_sheets_tool.google_sheet( + credentials_path, "ABR-run-data", 0 + ) + determine_lifetime(abr_google_sheet) + compare_run_to_temp_data(abr_data, temp_data, abr_google_sheet) + + if __name__ == "__main__": parser = argparse.ArgumentParser( description="Adds average robot ambient conditions to run sheet." @@ -225,21 +249,7 @@ def connect_and_download( help="Path to long term storage directory for run logs.", ) args = parser.parse_args() - google_sheets_to_download = { - "ABR-run-data": args.abr_data_sheet, - "ABR Ambient Conditions": args.room_conditions_sheet, - } - storage_directory = args.storage_directory - # Download google sheets. - file_paths, credentials_path = connect_and_download( - google_sheets_to_download, storage_directory - ) - # TODO: read csvs. - abr_data = read_csv_as_dict(file_paths[0]) - temp_data = read_csv_as_dict(file_paths[1]) - # TODO: compare robot and timestamps. - abr_google_sheet = google_sheets_tool.google_sheet( - credentials_path, "ABR-run-data", 0 - ) - determine_lifetime(abr_google_sheet) - compare_run_to_temp_data(abr_data, temp_data, abr_google_sheet) + storage_directory = args.storage_directory[0] + abr_data_sheet_url = args.abr_data_sheet[0] + room_conditions_sheet_url = args.room_conditions_sheet[0] + run(storage_directory, abr_data_sheet_url, room_conditions_sheet_url) diff --git a/abr-testing/protocol_simulation/__init__.py b/abr-testing/protocol_simulation/__init__.py deleted file mode 100644 index 157c21fd93e..00000000000 --- a/abr-testing/protocol_simulation/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The package holding code for simulating protocols.""" \ No newline at end of file diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json index a73a19e4c88..a2aca7e252a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9383,7 +9383,14 @@ }, "id": "UUID", "key": "08e16a2cac011d4bef561f8b0854d19e", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "displayName": "4 custom tubes", "loadName": "cpx_4_tuberack_100ul", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 56d23e7468e..e4924262e1a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9383,7 +9383,14 @@ }, "id": "UUID", "key": "08e16a2cac011d4bef561f8b0854d19e", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "displayName": "4 custom tubes", "loadName": "cpx_4_tuberack_100ul", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json index 0ba775ce4cb..f2c63721b33 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json @@ -7895,7 +7895,14 @@ }, "id": "UUID", "key": "675eb8be-6c85-4204-9c87-d7fdd522f580", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "moduleId": "UUID" }, diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json index 99501a91cd3..0b2e524dee6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4341,7 +4341,14 @@ }, "id": "UUID", "key": "bccdb28e967f574dfbe472004101d7f9", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "displayName": "Index Anchors", "loadName": "eppendorf_96_wellplate_150ul", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[19c783e363][Flex_X_v8_P1000_96_HS_GRIP_TC_TM_GripperCollisionWithTips].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[19c783e363][Flex_X_v8_P1000_96_HS_GRIP_TC_TM_GripperCollisionWithTips].json index 9f6db189f50..b7c2e8d8c6a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[19c783e363][Flex_X_v8_P1000_96_HS_GRIP_TC_TM_GripperCollisionWithTips].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[19c783e363][Flex_X_v8_P1000_96_HS_GRIP_TC_TM_GripperCollisionWithTips].json @@ -12587,7 +12587,14 @@ }, "id": "UUID", "key": "702caca4-12c8-4f26-a68e-138134723f09", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "labwareId": "UUID", "newLocation": { diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json index 77e30bb1865..351c26b64b4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json @@ -1772,7 +1772,14 @@ }, "id": "UUID", "key": "c2e4fa67-3c04-4d22-b3fa-5d61e956c488", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "flowRate": 3.78, "labwareId": "UUID", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b17883f74][OT2_S_v2_17_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b17883f74][OT2_S_v2_17_P300M_P20S_HS_TC_TM_dispense_changes].json index 8986f5e49cb..5af5922dada 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b17883f74][OT2_S_v2_17_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b17883f74][OT2_S_v2_17_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3035,7 +3035,14 @@ }, "id": "UUID", "key": "0bd3f36a944ee534e422ee69360a9501", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "flowRate": 7.56, "labwareId": "UUID", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json index 60a3d0150e4..d810bd75c88 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json @@ -32,7 +32,14 @@ }, "id": "UUID", "key": "8511b05ba5565bf0e6dcccd800e2ee23", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "location": { "slotName": "C1" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index 662d6cf0c4b..0aaa562c15c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -1205,7 +1205,14 @@ }, "id": "UUID", "key": "2c37ad797da7df791b57a7843a203e88", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "configurationParams": { "backLeftNozzle": "A1", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json index 75ea09b454d..c76b2aca7f9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json @@ -1181,7 +1181,14 @@ }, "id": "UUID", "key": "bd403a1c851a75b4b68ce34796d713fa", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "liquidPresenceDetection": false, "mount": "left", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json index 24e88e5454e..0de0eff0022 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json @@ -121,7 +121,14 @@ }, "id": "UUID", "key": "a3a7eed460d8d94a91747f23820a180d", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "location": { "slotName": "C3" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index a59e4a3176f..2c3d142321b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -2366,7 +2366,14 @@ }, "id": "UUID", "key": "4b1d27a6f17f312dd76668f0c48ed406", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "configurationParams": { "backLeftNozzle": "G12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json index eedcd721687..8d4e3a960dd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json @@ -1350,7 +1350,14 @@ }, "id": "UUID", "key": "4cca9753dc59d176eee1522349363a75", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "labwareId": "UUID", "newLocation": { diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json index 25cba8c59b8..ef9acd1b1a3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json @@ -15103,7 +15103,14 @@ }, "id": "UUID", "key": "c3eacf39e9a35058cac9f69100549344", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "forceDirect": false, "labwareId": "UUID", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index d70c634dcc6..c8389b97d75 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -2351,7 +2351,14 @@ }, "id": "UUID", "key": "4b1d27a6f17f312dd76668f0c48ed406", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "configurationParams": { "backLeftNozzle": "A1", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json index e542e8191b2..7005e6011ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4341,7 +4341,14 @@ }, "id": "UUID", "key": "bccdb28e967f574dfbe472004101d7f9", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "displayName": "Index Anchors", "loadName": "eppendorf_96_wellplate_150ul", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 13e42d0bd8b..1149640d8b1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -1220,7 +1220,14 @@ }, "id": "UUID", "key": "2c37ad797da7df791b57a7843a203e88", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "configurationParams": { "backLeftNozzle": "G12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json index 2de55429a53..b22e56cb8ed 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json @@ -39047,7 +39047,14 @@ }, "id": "UUID", "key": "f524340032354f66bf69110d530e98ad", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "labwareId": "UUID", "newLocation": { diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json index 5c1d9a41364..368bbe05d9b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json @@ -32,7 +32,14 @@ }, "id": "UUID", "key": "8511b05ba5565bf0e6dcccd800e2ee23", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "location": { "slotName": "C2" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json index 6e5b9d8028b..d1feceae4d0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json @@ -17824,7 +17824,14 @@ }, "id": "UUID", "key": "7e96139ed2163fa7f870805d0b3d14b6", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "forceDirect": false, "labwareId": "UUID", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json index 9fbcd62f394..d452cf7ab52 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json @@ -1255,7 +1255,14 @@ }, "id": "UUID", "key": "c55807b45b6b1d4ea04e12b0ee553f78", - "notes": [], + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], "params": { "location": { "slotName": "D3" diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index c1389ea6a5b..931c99fd4c6 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -13,7 +13,6 @@ Sequence, Iterator, TypeVar, - overload, ) import numpy @@ -503,25 +502,12 @@ def plunger_flowrate( ul_per_s = mm_per_s * instr.ul_per_mm(instr.liquid_class.max_volume, action) return round(ul_per_s, 6) - @overload def plan_check_aspirate( - self, mount: top_types.Mount, volume: Optional[float], rate: float - ) -> Optional[LiquidActionSpec]: - ... - - @overload - def plan_check_aspirate( - self, mount: OT3Mount, volume: Optional[float], rate: float - ) -> Optional[LiquidActionSpec]: - ... - - # note on this type ignore: see motion_utilities - def plan_check_aspirate( # type: ignore[no-untyped-def] self, - mount, - volume, - rate, - ): + mount: MountType, + volume: Optional[float], + rate: float, + ) -> Optional[LiquidActionSpec]: """Check preconditions for aspirate, parse args, and calculate positions. While the mechanics of issuing an aspirate move itself are left to child @@ -580,28 +566,12 @@ def plan_check_aspirate( # type: ignore[no-untyped-def] current=instrument.plunger_motor_current.run, ) - @overload def plan_check_dispense( self, - mount: top_types.Mount, - volume: Optional[float], - rate: float, - push_out: Optional[float], - ) -> Optional[LiquidActionSpec]: - ... - - @overload - def plan_check_dispense( - self, - mount: OT3Mount, + mount: MountType, volume: Optional[float], rate: float, push_out: Optional[float], - ) -> Optional[LiquidActionSpec]: - ... - - def plan_check_dispense( # type: ignore[no-untyped-def] - self, mount, volume, rate, push_out ) -> Optional[LiquidActionSpec]: """Check preconditions for dispense, parse args, and calculate positions. @@ -695,15 +665,7 @@ def plan_check_dispense( # type: ignore[no-untyped-def] current=instrument.plunger_motor_current.run, ) - @overload - def plan_check_blow_out(self, mount: top_types.Mount) -> LiquidActionSpec: - ... - - @overload - def plan_check_blow_out(self, mount: OT3Mount) -> LiquidActionSpec: - ... - - def plan_check_blow_out(self, mount): # type: ignore[no-untyped-def] + def plan_check_blow_out(self, mount: MountType) -> LiquidActionSpec: """Check preconditions and calculate values for blowout.""" instrument = self.get_pipette(mount) speed = self.plunger_speed( @@ -751,33 +713,13 @@ def build_one_shake() -> List[Tuple[top_types.Point, Optional[float]]]: else: return [] - @overload def plan_check_pick_up_tip( self, - mount: top_types.Mount, - presses: Optional[int], - increment: Optional[float], - tip_length: float = 0, - ) -> Tuple[PickUpTipSpec, Callable[[], None]]: - ... - - @overload - def plan_check_pick_up_tip( - self, - mount: OT3Mount, + mount: MountType, presses: Optional[int], increment: Optional[float], tip_length: float = 0, ) -> Tuple[PickUpTipSpec, Callable[[], None]]: - ... - - def plan_check_pick_up_tip( # type: ignore[no-untyped-def] - self, - mount, - presses, - increment, - tip_length=0, - ): # Prechecks: ready for pickup tip and press/increment are valid instrument = self.get_pipette(mount) if instrument.has_tip: @@ -925,25 +867,13 @@ def build() -> List[DropTipMove]: return build - @overload - def plan_check_drop_tip( - self, mount: top_types.Mount, home_after: bool - ) -> Tuple[DropTipSpec, Callable[[], None]]: - ... - - @overload - def plan_check_drop_tip( - self, mount: OT3Mount, home_after: bool - ) -> Tuple[DropTipSpec, Callable[[], None]]: - ... - # todo(mm, 2024-10-17): The returned _remove_tips() callable is not used by anything # anymore. Delete it. - def plan_check_drop_tip( # type: ignore[no-untyped-def] + def plan_check_drop_tip( self, - mount, - home_after, - ): + mount: MountType, + home_after: bool, + ) -> Tuple[DropTipSpec, Callable[[], None]]: instrument = self.get_pipette(mount) if not instrument.drop_configurations.plunger_eject: diff --git a/api/src/opentrons/protocol_engine/actions/__init__.py b/api/src/opentrons/protocol_engine/actions/__init__.py index dfd497817c0..26dfb0df8e0 100644 --- a/api/src/opentrons/protocol_engine/actions/__init__.py +++ b/api/src/opentrons/protocol_engine/actions/__init__.py @@ -30,7 +30,7 @@ SetPipetteMovementSpeedAction, AddAbsorbanceReaderLidAction, ) -from .get_state_update import get_state_update +from .get_state_update import get_state_updates __all__ = [ # action pipeline interface @@ -63,5 +63,5 @@ "PauseSource", "FinishErrorDetails", # helper functions - "get_state_update", + "get_state_updates", ] diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 4569f7866ef..4cdcb771616 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -69,7 +69,7 @@ class StopAction: class ResumeFromRecoveryAction: """See `ProtocolEngine.resume_from_recovery()`.""" - pass + state_update: StateUpdate @dataclasses.dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/actions/get_state_update.py b/api/src/opentrons/protocol_engine/actions/get_state_update.py index 3805ae642a5..ec29a6e38ef 100644 --- a/api/src/opentrons/protocol_engine/actions/get_state_update.py +++ b/api/src/opentrons/protocol_engine/actions/get_state_update.py @@ -2,20 +2,37 @@ from __future__ import annotations from typing import TYPE_CHECKING -from .actions import Action, SucceedCommandAction, FailCommandAction +from .actions import ( + Action, + ResumeFromRecoveryAction, + SucceedCommandAction, + FailCommandAction, +) from ..commands.command import DefinedErrorData +from ..error_recovery_policy import ErrorRecoveryType if TYPE_CHECKING: from ..state.update_types import StateUpdate -def get_state_update(action: Action) -> StateUpdate | None: - """Extract the StateUpdate from an action, if there is one.""" +def get_state_updates(action: Action) -> list[StateUpdate]: + """Extract all the StateUpdates that the StateStores should apply when they apply an action.""" if isinstance(action, SucceedCommandAction): - return action.state_update + return [action.state_update] + elif isinstance(action, FailCommandAction) and isinstance( action.error, DefinedErrorData ): - return action.error.state_update + if action.type == ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE: + return [ + action.error.state_update, + action.error.state_update_if_false_positive, + ] + else: + return [action.error.state_update] + + elif isinstance(action, ResumeFromRecoveryAction): + return [action.state_update] + else: - return None + return [] diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index 9ba9404af1f..813a038d7ec 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -147,6 +147,10 @@ class DefinedErrorData(Generic[_ErrorT_co]): ) """How the engine state should be updated to reflect this command failure.""" + state_update_if_false_positive: StateUpdate = dataclasses.field( + default_factory=StateUpdate + ) + class BaseCommand( GenericModel, diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index f4917a82195..114a97b0467 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -146,7 +146,15 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: ) ], ) - return DefinedErrorData(public=error, state_update=state_update) + state_update_if_false_positive = update_types.StateUpdate() + state_update_if_false_positive.update_pipette_tip_state( + pipette_id=params.pipetteId, tip_geometry=None + ) + return DefinedErrorData( + public=error, + state_update=state_update, + state_update_if_false_positive=state_update_if_false_positive, + ) else: state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py index 81b47e05c08..49d44d6b563 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py @@ -72,6 +72,10 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: pipette_id=params.pipetteId, home_after=params.homeAfter ) except TipAttachedError as exception: + state_update_if_false_positive = update_types.StateUpdate() + state_update_if_false_positive.update_pipette_tip_state( + pipette_id=params.pipetteId, tip_geometry=None + ) error = TipPhysicallyAttachedError( id=self._model_utils.generate_id(), createdAt=self._model_utils.get_timestamp(), @@ -83,7 +87,11 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: ) ], ) - return DefinedErrorData(public=error, state_update=state_update) + return DefinedErrorData( + public=error, + state_update=state_update, + state_update_if_false_positive=state_update_if_false_positive, + ) else: state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 5ccdcfc6f3a..bf8492cc74b 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -6,7 +6,7 @@ from typing_extensions import Literal -from ..errors import ErrorOccurrence, TipNotAttachedError +from ..errors import ErrorOccurrence, PickUpTipTipNotAttachedError from ..resources import ModelUtils from ..state import update_types from ..types import PickUpTipWellLocation, DeckPoint @@ -140,7 +140,12 @@ async def execute( labware_id=labware_id, well_name=well_name, ) - except TipNotAttachedError as e: + except PickUpTipTipNotAttachedError as e: + state_update_if_false_positive = update_types.StateUpdate() + state_update_if_false_positive.update_pipette_tip_state( + pipette_id=pipette_id, + tip_geometry=e.tip_geometry, + ) state_update.mark_tips_as_used( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name ) @@ -157,6 +162,7 @@ async def execute( ], ), state_update=state_update, + state_update_if_false_positive=state_update_if_false_positive, ) else: state_update.update_pipette_tip_state( diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index dc66591eff2..372972c1f50 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -5,12 +5,20 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import DoorState -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy +from opentrons.protocol_engine.execution.error_recovery_hardware_state_synchronizer import ( + ErrorRecoveryHardwareStateSynchronizer, +) from opentrons.util.async_helpers import async_context_manager_in_thread + from opentrons_shared_data.robot import load as load_robot +from .actions.action_dispatcher import ActionDispatcher +from .error_recovery_policy import ErrorRecoveryPolicy +from .execution.door_watcher import DoorWatcher +from .execution.hardware_stopper import HardwareStopper +from .plugins import PluginStarter from .protocol_engine import ProtocolEngine -from .resources import DeckDataProvider, ModuleDataProvider, FileProvider +from .resources import DeckDataProvider, ModuleDataProvider, FileProvider, ModelUtils from .state.config import Config from .state.state import StateStore from .types import PostRunHardwareState, DeckConfigurationType @@ -61,10 +69,27 @@ async def create_protocol_engine( deck_configuration=deck_configuration, notify_publishers=notify_publishers, ) + hardware_state_synchronizer = ErrorRecoveryHardwareStateSynchronizer( + hardware_api, state_store + ) + action_dispatcher = ActionDispatcher(state_store) + action_dispatcher.add_handler(hardware_state_synchronizer) + plugin_starter = PluginStarter(state_store, action_dispatcher) + model_utils = ModelUtils() + hardware_stopper = HardwareStopper(hardware_api, state_store) + door_watcher = DoorWatcher(state_store, hardware_api, action_dispatcher) + module_data_provider = ModuleDataProvider() + file_provider = file_provider or FileProvider() return ProtocolEngine( - state_store=state_store, hardware_api=hardware_api, + state_store=state_store, + action_dispatcher=action_dispatcher, + plugin_starter=plugin_starter, + model_utils=model_utils, + hardware_stopper=hardware_stopper, + door_watcher=door_watcher, + module_data_provider=module_data_provider, file_provider=file_provider, ) diff --git a/api/src/opentrons/protocol_engine/error_recovery_policy.py b/api/src/opentrons/protocol_engine/error_recovery_policy.py index d959651393e..fcc8a2ffef5 100644 --- a/api/src/opentrons/protocol_engine/error_recovery_policy.py +++ b/api/src/opentrons/protocol_engine/error_recovery_policy.py @@ -26,10 +26,20 @@ class ErrorRecoveryType(enum.Enum): """ WAIT_FOR_RECOVERY = enum.auto() - """Stop and wait for the error to be recovered from manually.""" + """Enter interactive error recovery mode.""" - IGNORE_AND_CONTINUE = enum.auto() - """Continue with the run, as if the command never failed.""" + CONTINUE_WITH_ERROR = enum.auto() + """Continue without interruption, carrying on from whatever error state the failed + command left the engine in. + + This is like `ProtocolEngine.resume_from_recovery(reconcile_false_positive=False)`. + """ + + ASSUME_FALSE_POSITIVE_AND_CONTINUE = enum.auto() + """Continue without interruption, acting as if the underlying error was a false positive. + + This is like `ProtocolEngine.resume_from_recovery(reconcile_false_positive=True)`. + """ class ErrorRecoveryPolicy(Protocol): diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 9bbe3aae9b8..b25dfdb2d0e 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -8,6 +8,7 @@ InvalidSpecificationForRobotTypeError, InvalidLoadPipetteSpecsError, TipNotAttachedError, + PickUpTipTipNotAttachedError, TipAttachedError, CommandDoesNotExistError, LabwareNotLoadedError, @@ -89,6 +90,7 @@ "InvalidSpecificationForRobotTypeError", "InvalidLoadPipetteSpecsError", "TipNotAttachedError", + "PickUpTipTipNotAttachedError", "TipAttachedError", "CommandDoesNotExistError", "LabwareNotLoadedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 5656942b338..12f45f4936d 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1,11 +1,17 @@ """Protocol engine exceptions.""" +from __future__ import annotations + from logging import getLogger -from typing import Any, Dict, Optional, Union, Iterator, Sequence +from typing import Any, Dict, Final, Optional, Union, Iterator, Sequence, TYPE_CHECKING from opentrons_shared_data.errors import ErrorCodes from opentrons_shared_data.errors.exceptions import EnumeratedError, PythonException +if TYPE_CHECKING: + from opentrons.protocol_engine.types import TipGeometry + + log = getLogger(__name__) @@ -132,6 +138,21 @@ def __init__( super().__init__(ErrorCodes.UNEXPECTED_TIP_REMOVAL, message, details, wrapping) +class PickUpTipTipNotAttachedError(TipNotAttachedError): + """Raised from TipHandler.pick_up_tip(). + + This is like TipNotAttachedError except that it carries some extra information + about the attempted operation. + """ + + tip_geometry: Final[TipGeometry] + """The tip geometry that would have been on the pipette, had the operation succeeded.""" + + def __init__(self, tip_geometry: TipGeometry) -> None: + super().__init__() + self.tip_geometry = tip_geometry + + class TipAttachedError(ProtocolEngineError): """Raised when a tip shouldn't be attached, but is.""" diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index 1d30b8756d2..e534001ef12 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -12,6 +12,7 @@ ) from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.notes import make_error_recovery_debug_note from ..state.state import StateStore from ..resources import ModelUtils, FileProvider @@ -161,6 +162,12 @@ async def execute(self, command_id: str) -> None: elif not isinstance(error, EnumeratedError): error = PythonException(error) + error_recovery_type = error_recovery_policy( + self._state_store.config, + running_command, + None, + ) + note_tracker(make_error_recovery_debug_note(error_recovery_type)) self._action_dispatcher.dispatch( FailCommandAction( error=error, @@ -169,11 +176,7 @@ async def execute(self, command_id: str) -> None: error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), notes=note_tracker.get_notes(), - type=error_recovery_policy( - self._state_store.config, - running_command, - None, - ), + type=error_recovery_type, ) ) @@ -195,6 +198,12 @@ async def execute(self, command_id: str) -> None: ) else: # The command encountered a defined error. + error_recovery_type = error_recovery_policy( + self._state_store.config, + running_command, + result, + ) + note_tracker(make_error_recovery_debug_note(error_recovery_type)) self._action_dispatcher.dispatch( FailCommandAction( error=result, @@ -203,10 +212,6 @@ async def execute(self, command_id: str) -> None: error_id=result.public.id, failed_at=result.public.createdAt, notes=note_tracker.get_notes(), - type=error_recovery_policy( - self._state_store.config, - running_command, - result, - ), + type=error_recovery_type, ) ) diff --git a/api/src/opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py b/api/src/opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py new file mode 100644 index 00000000000..67d75cfb181 --- /dev/null +++ b/api/src/opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py @@ -0,0 +1,101 @@ +# noqa: D100 + + +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.actions.action_handler import ActionHandler +from opentrons.protocol_engine.actions.actions import ( + Action, + FailCommandAction, + ResumeFromRecoveryAction, +) +from opentrons.protocol_engine.commands.command import DefinedErrorData +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.execution.tip_handler import HardwareTipHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView + + +class ErrorRecoveryHardwareStateSynchronizer(ActionHandler): + """A hack to keep the hardware API's state correct through certain error recovery flows. + + BACKGROUND: + + Certain parts of robot state are duplicated between `opentrons.protocol_engine` and + `opentrons.hardware_control`. Stuff like "is there a tip attached." + + Normally, Protocol Engine command implementations (`opentrons.protocol_engine.commands`) + mutate hardware API state when they execute; and then when they finish executing, + the Protocol Engine state stores (`opentrons.protocol_engine.state`) update Protocol + Engine state accordingly. So both halves are accounted for. This generally works fine. + + However, we need to go out of our way to support + `ProtocolEngine.resume_from_recovery(reconcile_false_positive=True)`. + It wants to apply a second set of state updates to "fix things up" with the + new knowledge that some error was a false positive. The Protocol Engine half of that + is easy for us to apply the normal way, through the state stores; but the + hardware API half of that cannot be applied the normal way, from the command + implementation, because the command in question is no longer running. + + THE HACK: + + This listens for the same error recovery state updates that the state stores do, + figures out what hardware API state mutations ought to go along with them, + and then does those mutations. + + The problem is that hardware API state is now mutated from two different places + (sometimes the command implementations, and sometimes here), which are bound + to grow accidental differences. + + TO FIX: + + Make Protocol Engine's use of the hardware API less stateful. e.g. supply + tip geometry every time we call a hardware API movement method, instead of + just once when we pick up a tip. Use Protocol Engine state as the single source + of truth. + """ + + def __init__(self, hardware_api: HardwareControlAPI, state_view: StateView) -> None: + self._hardware_api = hardware_api + self._state_view = state_view + + def handle_action(self, action: Action) -> None: + """Modify hardware API state in reaction to a Protocol Engine action.""" + state_update = _get_state_update(action) + if state_update: + self._synchronize(state_update) + + def _synchronize(self, state_update: update_types.StateUpdate) -> None: + tip_handler = HardwareTipHandler(self._state_view, self._hardware_api) + + if state_update.pipette_tip_state != update_types.NO_CHANGE: + pipette_id = state_update.pipette_tip_state.pipette_id + tip_geometry = state_update.pipette_tip_state.tip_geometry + if tip_geometry is None: + tip_handler.remove_tip(pipette_id) + else: + tip_handler.cache_tip(pipette_id=pipette_id, tip=tip_geometry) + + +def _get_state_update(action: Action) -> update_types.StateUpdate | None: + """Get the mutations that we need to do on the hardware API to stay in sync with an engine action. + + The mutations are returned in Protocol Engine terms, as a StateUpdate. + They then need to be converted to hardware API terms. + """ + match action: + case ResumeFromRecoveryAction(state_update=state_update): + return state_update + + case FailCommandAction( + error=DefinedErrorData( + state_update_if_false_positive=state_update_if_false_positive + ) + ): + return ( + state_update_if_false_positive + if action.type == ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE + else None + ) + + case _: + return None diff --git a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py index 24055f6b03b..81d4f10d94d 100644 --- a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py +++ b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py @@ -78,9 +78,7 @@ async def _drop_tip(self) -> None: try: if self._state_store.labware.get_fixed_trash_id() == FIXED_TRASH_ID: # OT-2 and Flex 2.15 protocols will default to the Fixed Trash Labware - await self._tip_handler.cache_tip( - pipette_id=pipette_id, tip=tip - ) + self._tip_handler.cache_tip(pipette_id=pipette_id, tip=tip) await self._movement_handler.move_to_well( pipette_id=pipette_id, labware_id=FIXED_TRASH_ID, @@ -92,9 +90,7 @@ async def _drop_tip(self) -> None: ) elif self._state_store.config.robot_type == "OT-2 Standard": # API 2.16 and above OT2 protocols use addressable areas - await self._tip_handler.cache_tip( - pipette_id=pipette_id, tip=tip - ) + self._tip_handler.cache_tip(pipette_id=pipette_id, tip=tip) await self._movement_handler.move_to_addressable_area( pipette_id=pipette_id, addressable_area_name="fixedTrash", diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index a963dd9abac..dde67ece007 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -4,6 +4,9 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import FailedTipStateCheck, InstrumentProbeType +from opentrons.protocol_engine.errors.exceptions import PickUpTipTipNotAttachedError +from opentrons.types import Mount + from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, CommandParameterLimitViolated, @@ -70,7 +73,7 @@ async def pick_up_tip( Tip geometry of the picked up tip. Raises: - TipNotAttachedError + PickUpTipTipNotAttachedError """ ... @@ -83,9 +86,12 @@ async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: TipAttachedError """ - async def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: + def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: """Tell the Hardware API that a tip is attached.""" + def remove_tip(self, pipette_id: str) -> None: + """Tell the hardware API that no tip is attached.""" + async def get_tip_presence(self, pipette_id: str) -> TipPresenceStatus: """Get tip presence status on the pipette.""" @@ -198,6 +204,11 @@ def __init__( self._labware_data_provider = labware_data_provider or LabwareDataProvider() self._state_view = state_view + # WARNING: ErrorRecoveryHardwareStateSynchronizer can currently construct several + # instances of this class per run, in addition to the main instance used + # for command execution. We're therefore depending on this class being + # stateless, so consider that before adding additional attributes here. + async def available_for_nozzle_layout( self, pipette_id: str, @@ -223,7 +234,7 @@ async def pick_up_tip( well_name: str, ) -> TipGeometry: """See documentation on abstract base class.""" - hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() + hw_mount = self._get_hw_mount(pipette_id) nominal_tip_geometry = self._state_view.geometry.get_nominal_tip_geometry( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name @@ -234,6 +245,7 @@ async def pick_up_tip( labware_definition=self._state_view.labware.get_definition(labware_id), nominal_fallback=nominal_tip_geometry.length, ) + tip_geometry = TipGeometry( length=actual_tip_length, diameter=nominal_tip_geometry.diameter, @@ -243,10 +255,12 @@ async def pick_up_tip( await self._hardware_api.tip_pickup_moves( mount=hw_mount, presses=None, increment=None ) - # Allow TipNotAttachedError to propagate. - await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) + try: + await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) + except TipNotAttachedError as e: + raise PickUpTipTipNotAttachedError(tip_geometry=tip_geometry) from e - await self.cache_tip(pipette_id, tip_geometry) + self.cache_tip(pipette_id, tip_geometry) await self._hardware_api.prepare_for_aspirate(hw_mount) @@ -254,7 +268,7 @@ async def pick_up_tip( async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: """See documentation on abstract base class.""" - hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() + hw_mount = self._get_hw_mount(pipette_id) # Let the hardware controller handle defaulting home_after since its behavior # differs between machines @@ -268,12 +282,11 @@ async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: # Allow TipNotAttachedError to propagate. await self.verify_tip_presence(pipette_id, TipPresenceStatus.ABSENT) - self._hardware_api.remove_tip(hw_mount) - self._hardware_api.set_current_tiprack_diameter(hw_mount, 0) + self.remove_tip(pipette_id) - async def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: + def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: """See documentation on abstract base class.""" - hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() + hw_mount = self._get_hw_mount(pipette_id) self._hardware_api.cache_tip(mount=hw_mount, tip_length=tip.length) @@ -287,12 +300,18 @@ async def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: tip_volume=tip.volume, ) + def remove_tip(self, pipette_id: str) -> None: + """See documentation on abstract base class.""" + hw_mount = self._get_hw_mount(pipette_id) + self._hardware_api.remove_tip(hw_mount) + self._hardware_api.set_current_tiprack_diameter(hw_mount, 0) + async def get_tip_presence(self, pipette_id: str) -> TipPresenceStatus: """See documentation on abstract base class.""" try: ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api) - hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() + hw_mount = self._get_hw_mount(pipette_id) status = await ot3api.get_tip_presence_status(hw_mount) return TipPresenceStatus.from_hw_state(status) @@ -333,7 +352,7 @@ async def verify_tip_presence( return try: ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api) - hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() + hw_mount = self._get_hw_mount(pipette_id) await ot3api.verify_tip_presence( hw_mount, expected.to_hw_state(), follow_singular_sensor ) @@ -351,6 +370,9 @@ async def verify_tip_presence( wrapping=[PythonException(e)], ) + def _get_hw_mount(self, pipette_id: str) -> Mount: + return self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() + class VirtualTipHandler(TipHandler): """Pick up and drop tips, using a virtual pipette.""" @@ -414,13 +436,20 @@ async def drop_tip( expected_has_tip=True, ) - async def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: - """Add a tip using a virtual pipette. + def cache_tip(self, pipette_id: str, tip: TipGeometry) -> None: + """See documentation on abstract base class. This should not be called when using virtual pipettes. """ assert False, "TipHandler.cache_tip should not be used with virtual pipettes" + def remove_tip(self, pipette_id: str) -> None: + """See documentation on abstract base class. + + This should not be called when using virtual pipettes. + """ + assert False, "TipHandler.remove_tip should not be used with virtual pipettes" + async def verify_tip_presence( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/notes/__init__.py b/api/src/opentrons/protocol_engine/notes/__init__.py index f5b1d8c1a2a..606d75665a4 100644 --- a/api/src/opentrons/protocol_engine/notes/__init__.py +++ b/api/src/opentrons/protocol_engine/notes/__init__.py @@ -1,5 +1,17 @@ """Protocol engine notes module.""" -from .notes import NoteKind, CommandNote, CommandNoteAdder, CommandNoteTracker +from .notes import ( + NoteKind, + CommandNote, + CommandNoteAdder, + CommandNoteTracker, + make_error_recovery_debug_note, +) -__all__ = ["NoteKind", "CommandNote", "CommandNoteAdder", "CommandNoteTracker"] +__all__ = [ + "NoteKind", + "CommandNote", + "CommandNoteAdder", + "CommandNoteTracker", + "make_error_recovery_debug_note", +] diff --git a/api/src/opentrons/protocol_engine/notes/notes.py b/api/src/opentrons/protocol_engine/notes/notes.py index cf381aa4a68..8c349d167cd 100644 --- a/api/src/opentrons/protocol_engine/notes/notes.py +++ b/api/src/opentrons/protocol_engine/notes/notes.py @@ -1,7 +1,10 @@ """Definitions of data and interface shapes for notes.""" -from typing import Union, Literal, Protocol, List +from typing import Union, Literal, Protocol, List, TYPE_CHECKING from pydantic import BaseModel, Field +if TYPE_CHECKING: + from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType + NoteKind = Union[Literal["warning", "information"], str] @@ -26,6 +29,20 @@ class CommandNote(BaseModel): ) +def make_error_recovery_debug_note(type: "ErrorRecoveryType") -> CommandNote: + """Return a note for debugging error recovery. + + This is intended to be read by developers and support people, not computers. + """ + message = f"Handling this command failure with {type.name}." + return CommandNote.construct( + noteKind="debugErrorRecovery", + shortMessage=message, + longMessage=message, + source="execution", + ) + + class CommandNoteAdder(Protocol): """The shape of a function that something can use to add a command note.""" diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index d93ab5dd42d..ced32b20cc3 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -6,7 +6,6 @@ ResumeFromRecoveryAction, SetErrorRecoveryPolicyAction, ) -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy from opentrons.protocols.models import LabwareDefinition from opentrons.hardware_control import HardwareControlAPI @@ -19,6 +18,7 @@ from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllowedError from .errors.exceptions import EStopActivatedError +from .error_recovery_policy import ErrorRecoveryPolicy from . import commands, slot_standardization from .resources import ModelUtils, ModuleDataProvider, FileProvider from .types import ( @@ -39,6 +39,7 @@ HardwareStopper, ) from .state.state import StateStore, StateView +from .state.update_types import StateUpdate from .plugins import AbstractPlugin, PluginStarter from .actions import ( ActionDispatcher, @@ -88,43 +89,31 @@ def __init__( self, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: Optional[ActionDispatcher] = None, - plugin_starter: Optional[PluginStarter] = None, + action_dispatcher: ActionDispatcher, + plugin_starter: PluginStarter, + model_utils: ModelUtils, + hardware_stopper: HardwareStopper, + door_watcher: DoorWatcher, + module_data_provider: ModuleDataProvider, + file_provider: FileProvider, queue_worker: Optional[QueueWorker] = None, - model_utils: Optional[ModelUtils] = None, - hardware_stopper: Optional[HardwareStopper] = None, - door_watcher: Optional[DoorWatcher] = None, - module_data_provider: Optional[ModuleDataProvider] = None, - file_provider: Optional[FileProvider] = None, ) -> None: """Initialize a ProtocolEngine instance. Must be called while an event loop is active. - This constructor does not inject provider implementations. + This constructor is only for `ProtocolEngine` unit tests. Prefer the `create_protocol_engine()` factory function. """ self._hardware_api = hardware_api - self._file_provider = file_provider or FileProvider() + self._file_provider = file_provider self._state_store = state_store - self._model_utils = model_utils or ModelUtils() - self._action_dispatcher = action_dispatcher or ActionDispatcher( - sink=self._state_store - ) - self._plugin_starter = plugin_starter or PluginStarter( - state=self._state_store, - action_dispatcher=self._action_dispatcher, - ) - self._hardware_stopper = hardware_stopper or HardwareStopper( - hardware_api=hardware_api, - state_store=state_store, - ) - self._door_watcher = door_watcher or DoorWatcher( - state_store=state_store, - hardware_api=hardware_api, - action_dispatcher=self._action_dispatcher, - ) - self._module_data_provider = module_data_provider or ModuleDataProvider() + self._model_utils = model_utils + self._action_dispatcher = action_dispatcher + self._plugin_starter = plugin_starter + self._hardware_stopper = hardware_stopper + self._door_watcher = door_watcher + self._module_data_provider = module_data_provider self._queue_worker = queue_worker if self._queue_worker: self._queue_worker.start() @@ -186,11 +175,35 @@ def request_pause(self) -> None: self._action_dispatcher.dispatch(action) self._hardware_api.pause(HardwarePauseType.PAUSE) - def resume_from_recovery(self) -> None: - """Resume normal protocol execution after the engine was `AWAITING_RECOVERY`.""" + def resume_from_recovery(self, reconcile_false_positive: bool) -> None: + """Resume normal protocol execution after the engine was `AWAITING_RECOVERY`. + + If `reconcile_false_positive` is `False`, the engine will continue naively from + whatever state the error left it in. (Each defined error individually documents + exactly how it affects state.) This is appropriate for client-driven error + recovery, where the client wants predictable behavior from the engine. + + If `reconcile_false_positive` is `True`, the engine may apply additional fixups + to its state to try to get the rest of the run to just work, assuming the error + was a false-positive. + + For example, a `tipPhysicallyMissing` error from a `pickUpTip` would normally + leave the engine state without a tip on the pipette. If `reconcile_false_positive=True`, + the engine will set the pipette to have that missing tip before continuing, so + subsequent path planning, aspirates, dispenses, etc. will work as if nothing + went wrong. + """ + if reconcile_false_positive: + state_update = ( + self._state_store.commands.get_state_update_for_false_positive() + ) + else: + state_update = StateUpdate() # Empty/no-op. + action = self._state_store.commands.validate_action_allowed( - ResumeFromRecoveryAction() + ResumeFromRecoveryAction(state_update) ) + self._action_dispatcher.dispatch(action) def add_command( diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 6723c521892..dc9e0c7ee49 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -25,6 +25,7 @@ ErrorRecoveryType, ) from opentrons.protocol_engine.notes.notes import CommandNote +from opentrons.protocol_engine.state import update_types from ..actions import ( Action, @@ -141,6 +142,16 @@ class CommandPointer: index: int +@dataclass(frozen=True) +class _RecoveryTargetInfo: + """Info about the failed command that we're currently recovering from.""" + + command_id: str + + state_update_if_false_positive: update_types.StateUpdate + """See `CommandView.get_state_update_if_continued()`.""" + + @dataclass class CommandState: """State of all protocol engine command resources.""" @@ -205,8 +216,8 @@ class CommandState: stable. Eventually, we might want this info to be stored directly on each command. """ - recovery_target_command_id: Optional[str] - """If we're currently recovering from a command failure, which command it was.""" + recovery_target: Optional[_RecoveryTargetInfo] + """If we're currently recovering from a command failure, info about that command.""" finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" @@ -253,7 +264,7 @@ def __init__( finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_completed_at=None, run_started_at=None, latest_protocol_command_hash=None, @@ -335,14 +346,17 @@ def _handle_succeed_command_action(self, action: SucceedCommandAction) -> None: def _handle_fail_command_action(self, action: FailCommandAction) -> None: prev_entry = self.state.command_history.get(action.command_id) - if isinstance(action.error, EnumeratedError): + if isinstance(action.error, EnumeratedError): # The error was undefined. public_error_occurrence = ErrorOccurrence.from_failed( id=action.error_id, createdAt=action.failed_at, error=action.error, ) - else: + # An empty state update, to no-op. + state_update_if_false_positive = update_types.StateUpdate() + else: # The error was defined. public_error_occurrence = action.error.public + state_update_if_false_positive = action.error.state_update_if_false_positive self._update_to_failed( command_id=action.command_id, @@ -354,6 +368,19 @@ def _handle_fail_command_action(self, action: FailCommandAction) -> None: self._state.failed_command = self._state.command_history.get(action.command_id) self._state.failed_command_errors.append(public_error_occurrence) + if ( + prev_entry.command.intent in (CommandIntent.PROTOCOL, None) + and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY + ): + self._state.queue_status = QueueStatus.AWAITING_RECOVERY + self._state.recovery_target = _RecoveryTargetInfo( + command_id=action.command_id, + state_update_if_false_positive=state_update_if_false_positive, + ) + self._state.has_entered_error_recovery = True + + # When one command fails, we generally also cancel the commands that + # would have been queued after it. other_command_ids_to_fail: List[str] if prev_entry.command.intent == CommandIntent.SETUP: other_command_ids_to_fail = list( @@ -373,7 +400,8 @@ def _handle_fail_command_action(self, action: FailCommandAction) -> None: ) elif ( action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY - or action.type == ErrorRecoveryType.IGNORE_AND_CONTINUE + or action.type == ErrorRecoveryType.CONTINUE_WITH_ERROR + or action.type == ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE ): other_command_ids_to_fail = [] else: @@ -390,14 +418,6 @@ def _handle_fail_command_action(self, action: FailCommandAction) -> None: notes=None, ) - if ( - prev_entry.command.intent in (CommandIntent.PROTOCOL, None) - and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY - ): - self._state.queue_status = QueueStatus.AWAITING_RECOVERY - self._state.recovery_target_command_id = action.command_id - self._state.has_entered_error_recovery = True - def _handle_play_action(self, action: PlayAction) -> None: if not self._state.run_result: self._state.run_started_at = ( @@ -425,11 +445,11 @@ def _handle_resume_from_recovery_action( self, action: ResumeFromRecoveryAction ) -> None: self._state.queue_status = QueueStatus.RUNNING - self._state.recovery_target_command_id = None + self._state.recovery_target = None def _handle_stop_action(self, action: StopAction) -> None: if not self._state.run_result: - self._state.recovery_target_command_id = None + self._state.recovery_target = None self._state.queue_status = QueueStatus.PAUSED if action.from_estop: @@ -866,11 +886,11 @@ def get_all_commands_final(self) -> bool: def get_recovery_target(self) -> Optional[CommandPointer]: """Return the command currently undergoing error recovery, if any.""" - recovery_target_command_id = self._state.recovery_target_command_id - if recovery_target_command_id is None: + recovery_target = self._state.recovery_target + if recovery_target is None: return None else: - entry = self._state.command_history.get(recovery_target_command_id) + entry = self._state.command_history.get(recovery_target.command_id) return CommandPointer( command_id=entry.command.id, command_key=entry.command.key, @@ -1083,6 +1103,19 @@ def get_error_recovery_policy(self) -> ErrorRecoveryPolicy: """ return self._state.error_recovery_policy + def get_state_update_for_false_positive(self) -> update_types.StateUpdate: + """Return the state update for if the current recovery target was a false positive. + + If we're currently in error recovery mode, and you have decided that the + underlying command error was a false positive, this returns a state update + that will undo the error's effects on engine state. + See `ProtocolEngine.resume_from_recovery(reconcile_false_positive=True)`. + """ + if self._state.recovery_target is None: + return update_types.StateUpdate() # Empty/no-op. + else: + return self._state.recovery_target.state_update_if_false_positive + def _may_run_with_door_open( self, *, fixit_command: Command | CommandCreate ) -> bool: diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index dad9fe54dd0..7cea4f9765b 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -53,7 +53,7 @@ Action, AddLabwareOffsetAction, AddLabwareDefinitionAction, - get_state_update, + get_state_updates, ) from ._abstract_store import HasState, HandlesActions from ._move_types import EdgePathType @@ -149,8 +149,7 @@ def __init__( def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - state_update = get_state_update(action) - if state_update is not None: + for state_update in get_state_updates(action): self._add_loaded_labware(state_update) self._set_labware_location(state_update) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index ced8b6076f7..bb90e067ec6 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -39,7 +39,7 @@ FailCommandAction, SetPipetteMovementSpeedAction, SucceedCommandAction, - get_state_update, + get_state_updates, ) from ._abstract_store import HasState, HandlesActions @@ -141,8 +141,7 @@ def __init__(self) -> None: def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - state_update = get_state_update(action) - if state_update is not None: + for state_update in get_state_updates(action): self._set_load_pipette(state_update) self._update_current_location(state_update) self._update_pipette_config(state_update) diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index f744b1a01b4..7427c78ac4c 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -6,7 +6,7 @@ from opentrons.protocol_engine.state import update_types from ._abstract_store import HasState, HandlesActions -from ..actions import Action, SucceedCommandAction, ResetTipsAction, get_state_update +from ..actions import Action, SucceedCommandAction, ResetTipsAction, get_state_updates from ..commands import ( Command, LoadLabwareResult, @@ -63,8 +63,7 @@ def __init__(self) -> None: def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - state_update = get_state_update(action) - if state_update is not None: + for state_update in get_state_updates(action): self._handle_state_update(state_update) if isinstance(action, SucceedCommandAction): diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index dcf4f224811..aec2aae80df 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -123,9 +123,9 @@ async def stop(self) -> None: post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE, ) - def resume_from_recovery(self) -> None: + def resume_from_recovery(self, reconcile_false_positive: bool) -> None: """See `ProtocolEngine.resume_from_recovery()`.""" - self._protocol_engine.resume_from_recovery() + self._protocol_engine.resume_from_recovery(reconcile_false_positive) @abstractmethod async def run( diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 69d9feaf524..dfa66e6a55a 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -205,9 +205,9 @@ async def stop(self) -> None: post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE, ) - def resume_from_recovery(self) -> None: + def resume_from_recovery(self, reconcile_false_positive: bool) -> None: """Resume the run from recovery.""" - self._protocol_engine.resume_from_recovery() + self._protocol_engine.resume_from_recovery(reconcile_false_positive) async def finish( self, diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 4a8e32c05d0..8ba2c9c1d97 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -294,4 +294,10 @@ async def test_tip_attached_error( new_deck_point=DeckPoint(x=111, y=222, z=333), ), ), + state_update_if_false_positive=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="abc", + tip_geometry=None, + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py index f2061c3d552..fde82626969 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py @@ -91,4 +91,7 @@ async def test_tip_attached_error( wrappedErrors=[matchers.Anything()], ), state_update=StateUpdate(), + state_update_if_false_positive=StateUpdate( + pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 3771fe00eb1..2203f514cf4 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -2,6 +2,7 @@ from datetime import datetime from decoy import Decoy, matchers +from unittest.mock import sentinel from opentrons.types import MountType, Point @@ -11,7 +12,7 @@ WellOffset, DeckPoint, ) -from opentrons.protocol_engine.errors import TipNotAttachedError +from opentrons.protocol_engine.errors import PickUpTipTipNotAttachedError from opentrons.protocol_engine.execution import MovementHandler, TipHandler from opentrons.protocol_engine.resources import ModelUtils from opentrons.protocol_engine.state import update_types @@ -140,7 +141,7 @@ async def test_tip_physically_missing_error( await tip_handler.pick_up_tip( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name ) - ).then_raise(TipNotAttachedError()) + ).then_raise(PickUpTipTipNotAttachedError(tip_geometry=sentinel.tip_geometry)) decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_created_at) @@ -164,4 +165,9 @@ async def test_tip_physically_missing_error( pipette_id="pipette-id", labware_id="labware-id", well_name="well-name" ), ), + state_update_if_false_positive=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", tip_geometry=sentinel.tip_geometry + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py index d6c69d0b170..503d681bced 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py +++ b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py @@ -158,7 +158,7 @@ async def test_hardware_stopping_sequence_no_tip_drop( decoy.verify(await hardware_api.stop(home_after=False), times=1) decoy.verify( - await mock_tip_handler.cache_tip( + mock_tip_handler.cache_tip( pipette_id="pipette-id", tip=TipGeometry(length=1.0, volume=2.0, diameter=3.0), ), @@ -181,7 +181,7 @@ async def test_hardware_stopping_sequence_no_pipette( ) decoy.when( - await mock_tip_handler.cache_tip( + mock_tip_handler.cache_tip( pipette_id="pipette-id", tip=TipGeometry(length=1.0, volume=2.0, diameter=3.0), ), @@ -271,7 +271,7 @@ async def test_hardware_stopping_sequence_with_fixed_trash( await movement.home( axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] ), - await mock_tip_handler.cache_tip( + mock_tip_handler.cache_tip( pipette_id="pipette-id", tip=TipGeometry(length=1.0, volume=2.0, diameter=3.0), ), @@ -320,7 +320,7 @@ async def test_hardware_stopping_sequence_with_OT2_addressable_area( await movement.home( axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] ), - await mock_tip_handler.cache_tip( + mock_tip_handler.cache_tip( pipette_id="pipette-id", tip=TipGeometry(length=1.0, volume=2.0, diameter=3.0), ), diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 8ddb8840597..c03a611966c 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -266,7 +266,7 @@ async def test_drop_tip( ) -async def test_add_tip( +def test_add_tip( decoy: Decoy, mock_state_view: StateView, mock_hardware_api: HardwareAPI, @@ -289,7 +289,7 @@ async def test_add_tip( MountType.LEFT ) - await subject.cache_tip(pipette_id="pipette-id", tip=tip) + subject.cache_tip(pipette_id="pipette-id", tip=tip) decoy.verify( mock_hardware_api.cache_tip(mount=Mount.LEFT, tip_length=50), @@ -301,6 +301,31 @@ async def test_add_tip( ) +def test_remove_tip( + decoy: Decoy, + mock_state_view: StateView, + mock_hardware_api: HardwareAPI, + mock_labware_data_provider: LabwareDataProvider, +) -> None: + """It should remove a tip manually from the hardware API.""" + subject = HardwareTipHandler( + state_view=mock_state_view, + hardware_api=mock_hardware_api, + labware_data_provider=mock_labware_data_provider, + ) + + decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( + MountType.LEFT + ) + + subject.remove_tip(pipette_id="pipette-id") + + decoy.verify( + mock_hardware_api.remove_tip(Mount.LEFT), + mock_hardware_api.set_current_tiprack_diameter(Mount.LEFT, 0), + ) + + @pytest.mark.parametrize( argnames=[ "test_channels", diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py index 6f090612a74..9df52541f02 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -19,7 +19,7 @@ PlayAction, SetErrorRecoveryPolicyAction, ) -from opentrons.protocol_engine.commands.command import CommandIntent +from opentrons.protocol_engine.commands.command import CommandIntent, DefinedErrorData from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.errors.exceptions import ( @@ -32,6 +32,7 @@ CommandView, ) from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.state.update_types import StateUpdate from opentrons.protocol_engine.types import DeckType, EngineStatus @@ -772,7 +773,7 @@ def test_recovery_target_tracking() -> None: assert recovery_target.command_id == "c1" assert subject_view.get_recovery_in_progress_for_command("c1") - resume_from_1_recovery = actions.ResumeFromRecoveryAction() + resume_from_1_recovery = actions.ResumeFromRecoveryAction(StateUpdate()) subject.handle_action(resume_from_1_recovery) # c1 failed recoverably, but we've already completed its recovery. @@ -808,7 +809,7 @@ def test_recovery_target_tracking() -> None: # even though it failed recoverably before. assert not subject_view.get_recovery_in_progress_for_command("c1") - resume_from_2_recovery = actions.ResumeFromRecoveryAction() + resume_from_2_recovery = actions.ResumeFromRecoveryAction(StateUpdate()) subject.handle_action(resume_from_2_recovery) queue_3 = actions.QueueCommandAction( "c3", @@ -993,3 +994,57 @@ def test_set_and_get_error_recovery_policy() -> None: assert subject_view.get_error_recovery_policy() is initial_policy subject.handle_action(SetErrorRecoveryPolicyAction(sentinel.new_policy)) assert subject_view.get_error_recovery_policy() is new_policy + + +def test_get_state_update_for_false_positive() -> None: + """Test storage of false-positive state updates.""" + subject = CommandStore( + config=_make_config(), + error_recovery_policy=_placeholder_error_recovery_policy, + is_door_open=False, + ) + subject_view = CommandView(subject.state) + + empty_state_update = StateUpdate() + + assert subject_view.get_state_update_for_false_positive() == empty_state_update + + queue = actions.QueueCommandAction( + request=commands.CommentCreate( + params=commands.CommentParams(message=""), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue) + run = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run) + fail = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=DefinedErrorData( + public=sentinel.public, + state_update_if_false_positive=sentinel.state_update_if_false_positive, + ), + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + notes=[], + ) + subject.handle_action(fail) + + assert ( + subject_view.get_state_update_for_false_positive() + == sentinel.state_update_if_false_positive + ) + + resume_from_recovery = actions.ResumeFromRecoveryAction( + state_update=sentinel.some_other_state_update + ) + subject.handle_action(resume_from_recovery) + + assert subject_view.get_state_update_for_false_positive() == empty_state_update diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index 4b7cf01e87c..5dc3c4a4ee9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -334,7 +334,7 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, latest_protocol_command_hash=None, stopped_by_estop=False, failed_command_errors=[], @@ -363,7 +363,7 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, @@ -398,7 +398,7 @@ def test_command_store_handles_finish_action() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, @@ -453,7 +453,7 @@ def test_command_store_handles_stop_action( finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=from_estop, @@ -491,7 +491,7 @@ def test_command_store_handles_stop_action_when_awaiting_recovery() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, @@ -525,7 +525,7 @@ def test_command_store_cannot_restart_after_should_stop() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, @@ -673,7 +673,7 @@ def test_command_store_wraps_unknown_errors() -> None: run_started_at=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, latest_protocol_command_hash=None, stopped_by_estop=False, failed_command_errors=[], @@ -742,7 +742,7 @@ def __init__(self, message: str) -> None: ), failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, @@ -778,7 +778,7 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, @@ -814,7 +814,7 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, @@ -850,7 +850,7 @@ def test_handles_hardware_stopped() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, - recovery_target_command_id=None, + recovery_target=None, run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 06318cb8d36..f7b1d6cd31f 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -22,6 +22,9 @@ from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.state.commands import ( + # todo(mm, 2024-10-24): Avoid testing internal implementation details like + # _RecoveryTargetInfo. See note above about porting to test_command_state.py. + _RecoveryTargetInfo, CommandState, CommandView, CommandSlice, @@ -38,6 +41,7 @@ from opentrons_shared_data.errors.codes import ErrorCodes from opentrons.protocol_engine.state.command_history import CommandHistory +from opentrons.protocol_engine.state.update_types import StateUpdate from .command_fixtures import ( create_queued_command, @@ -108,7 +112,12 @@ def get_command_view( # noqa: C901 finish_error=finish_error, failed_command=failed_command, command_error_recovery_types=command_error_recovery_types or {}, - recovery_target_command_id=recovery_target_command_id, + recovery_target=_RecoveryTargetInfo( + command_id=recovery_target_command_id, + state_update_if_false_positive=StateUpdate(), + ) + if recovery_target_command_id is not None + else None, run_started_at=run_started_at, latest_protocol_command_hash=latest_command_hash, stopped_by_estop=False, @@ -592,7 +601,7 @@ class ActionAllowedSpec(NamedTuple): ), ), ), - action=ResumeFromRecoveryAction(), + action=ResumeFromRecoveryAction(StateUpdate()), expected_error=errors.ResumeFromRecoveryNotAllowedError, ), ActionAllowedSpec( diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 71e23cfe715..ac83e987153 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -10,6 +10,7 @@ from opentrons_shared_data.robot.types import RobotType from opentrons.protocol_engine.actions.actions import SetErrorRecoveryPolicyAction +from opentrons.protocol_engine.state.update_types import StateUpdate from opentrons.types import DeckSlotName from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI from opentrons.hardware_control.modules import MagDeck, TempDeck @@ -38,7 +39,11 @@ HardwareStopper, DoorWatcher, ) -from opentrons.protocol_engine.resources import ModelUtils, ModuleDataProvider +from opentrons.protocol_engine.resources import ( + FileProvider, + ModelUtils, + ModuleDataProvider, +) from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.plugins import AbstractPlugin, PluginStarter @@ -118,6 +123,12 @@ def module_data_provider(decoy: Decoy) -> ModuleDataProvider: return decoy.mock(cls=ModuleDataProvider) +@pytest.fixture +def file_provider(decoy: Decoy) -> FileProvider: + """Get a mock FileProvider.""" + return decoy.mock(cls=FileProvider) + + @pytest.fixture(autouse=True) def _mock_slot_standardization_module( decoy: Decoy, monkeypatch: pytest.MonkeyPatch @@ -148,6 +159,7 @@ def subject( hardware_stopper: HardwareStopper, door_watcher: DoorWatcher, module_data_provider: ModuleDataProvider, + file_provider: FileProvider, ) -> ProtocolEngine: """Get a ProtocolEngine test subject with its dependencies stubbed out.""" return ProtocolEngine( @@ -160,6 +172,7 @@ def subject( hardware_stopper=hardware_stopper, door_watcher=door_watcher, module_data_provider=module_data_provider, + file_provider=file_provider, ) @@ -613,20 +626,31 @@ def test_pause( ) +@pytest.mark.parametrize("reconcile_false_positive", [True, False]) def test_resume_from_recovery( decoy: Decoy, state_store: StateStore, action_dispatcher: ActionDispatcher, subject: ProtocolEngine, + reconcile_false_positive: bool, ) -> None: """It should dispatch a ResumeFromRecoveryAction.""" - expected_action = ResumeFromRecoveryAction() + decoy.when(state_store.commands.get_state_update_for_false_positive()).then_return( + sentinel.state_update_for_false_positive + ) + empty_state_update = StateUpdate() + + expected_action = ResumeFromRecoveryAction( + sentinel.state_update_for_false_positive + if reconcile_false_positive + else empty_state_update + ) decoy.when( state_store.commands.validate_action_allowed(expected_action) ).then_return(expected_action) - subject.resume_from_recovery() + subject.resume_from_recovery(reconcile_false_positive) decoy.verify(action_dispatcher.dispatch(expected_action)) diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index cd945c33e64..2f06e27c2c2 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -313,9 +313,16 @@ def test_resume_from_recovery( subject: AnyRunner, ) -> None: """It should call `resume_from_recovery()` on the underlying engine.""" - subject.resume_from_recovery() + subject.resume_from_recovery( + reconcile_false_positive=sentinel.reconcile_false_positive + ) - decoy.verify(protocol_engine.resume_from_recovery(), times=1) + decoy.verify( + protocol_engine.resume_from_recovery( + reconcile_false_positive=sentinel.reconcile_false_positive + ), + times=1, + ) async def test_run_json_runner( diff --git a/app-shell-odd/src/main.ts b/app-shell-odd/src/main.ts index b0f285fa194..ccb9ff61aa2 100644 --- a/app-shell-odd/src/main.ts +++ b/app-shell-odd/src/main.ts @@ -24,6 +24,7 @@ import { closeBrokerConnection, } from './notifications' import { setUserDataPath } from './early' +import { registerResourceMonitor } from './monitor' import type { OTLogger } from './log' import type { BrowserWindow } from 'electron' @@ -135,6 +136,7 @@ function startUp(): void { registerConfig(dispatch), registerDiscovery(dispatch), registerRobotSystemUpdate(dispatch), + registerResourceMonitor(dispatch), registerAppRestart(), registerUpdateBrightness(), registerNotify(dispatch, mainWindow), diff --git a/app-shell-odd/src/monitor/ResourceMonitor.ts b/app-shell-odd/src/monitor/ResourceMonitor.ts new file mode 100644 index 00000000000..e093ccbb716 --- /dev/null +++ b/app-shell-odd/src/monitor/ResourceMonitor.ts @@ -0,0 +1,305 @@ +import { exec } from 'child_process' +import { promises as fs } from 'fs' +import path from 'path' + +import { createLogger } from '../log' +import { UI_INITIALIZED } from '../constants' + +import type { Action, Dispatch } from '../types' + +export const PARENT_PROCESSES = [ + 'opentrons-robot-server.service', + 'opentrons-robot-app.service', +] as const +const REPORTING_INTERVAL_MS = 3600000 // 1 hour +const MAX_CMD_STR_LENGTH = 100 +const MAX_REPORTED_PROCESSES = 15 + +interface ProcessTreeNode { + pid: number + cmd: string + children: ProcessTreeNode[] +} + +interface ProcessDetails { + name: string + memRssMb: string +} + +interface ResourceMonitorDetails { + systemAvailMemMb: string + systemUptimeHrs: string + processesDetails: ProcessDetails[] +} + +interface ResourceMonitorOptions { + procPath?: string +} + +// Scrapes system and select process resource metrics, reporting those metrics to the browser layer. +// Note that only MAX_REPORTED_PROCESSES are actually dispatched. +export class ResourceMonitor { + private readonly monitoredProcesses: Set + private readonly log: ReturnType + private readonly procPath: string + private intervalId: NodeJS.Timeout | null + + constructor(options: ResourceMonitorOptions = {}) { + this.monitoredProcesses = new Set(PARENT_PROCESSES) + this.log = createLogger('monitor') + this.intervalId = null + this.procPath = options.procPath ?? '/proc' // Override used for testing purposes. + } + + start(dispatch: Dispatch): Dispatch { + // Scrape and report metrics on an interval. + const beginMonitor = (): void => { + if (this.intervalId == null) { + this.intervalId = setInterval(() => { + this.getResourceDetails() + .then(resourceDetails => { + this.log.debug('resource monitor report', { + resourceDetails, + }) + this.dispatchResourceDetails(resourceDetails, dispatch) + }) + .catch(error => { + this.log.error('Error monitoring process: ', error) + }) + }, REPORTING_INTERVAL_MS) + } else { + this.log.warn( + 'Attempted to start an already started instance of ResourceMonitor.' + ) + } + } + + return function handleAction(action: Action) { + switch (action.type) { + case UI_INITIALIZED: + beginMonitor() + } + } + } + + // Manually stop reporting, clearing internal state. + stop(): void { + if (this.intervalId != null) { + clearInterval(this.intervalId) + this.intervalId = null + this.monitoredProcesses.clear() + } + } + + private dispatchResourceDetails( + details: ResourceMonitorDetails, + dispatch: Dispatch + ): void { + const { processesDetails, systemUptimeHrs, systemAvailMemMb } = details + dispatch({ + type: 'analytics:RESOURCE_MONITOR_REPORT', + payload: { + systemUptimeHrs, + systemAvailMemMb, + processesDetails: processesDetails.slice(0, MAX_REPORTED_PROCESSES), // don't accidentally send too many items to mixpanel. + }, + }) + } + + private getResourceDetails(): Promise { + return Promise.all([ + this.getSystemAvailableMemory(), + this.getSystemUptimeHrs(), + this.getProcessDetails(), + ]).then(([systemAvailMemMb, systemUptimeHrs, processesDetails]) => ({ + systemAvailMemMb, + systemUptimeHrs, + processesDetails, + })) + } + + // Scrape system uptime from /proc/uptime. + private getSystemUptimeHrs(): Promise { + return fs + .readFile(path.join(this.procPath, 'uptime'), 'utf8') + .then(uptime => { + // First value is uptime in seconds, second is idle time + const uptimeSeconds = Math.floor(parseFloat(uptime.split(' ')[0])) + return (uptimeSeconds / 3600).toFixed(2) + }) + .catch(error => { + throw new Error( + `Failed to read system uptime: ${ + error instanceof Error ? error.message : String(error) + }` + ) + }) + } + + // Scrape system available memory from /proc/meminfo. + private getSystemAvailableMemory(): Promise { + return fs + .readFile(path.join(this.procPath, 'meminfo'), 'utf8') + .then(meminfo => { + const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/) + if (match == null) { + throw new Error('Could not find MemAvailable in meminfo file') + } else { + const memInKb = parseInt(match[1], 10) + return (memInKb / 1024).toFixed(2) + } + }) + .catch(error => { + throw new Error( + `Failed to read available memory info: ${ + error instanceof Error ? error.message : String(error) + }` + ) + }) + } + + // Given parent process names, get metrics for parent and all spawned processes. + private getProcessDetails(): Promise { + return Promise.all( + Array.from(this.monitoredProcesses).map(parentProcess => + this.getProcessTree(parentProcess) + .then(processTree => { + if (processTree == null) { + return [] + } else { + return this.getProcessDetailsFlattened(processTree) + } + }) + .catch(error => { + this.log.error('Failed to get process tree', { + parentProcess, + error, + }) + return [] + }) + ) + ).then(detailsArrays => detailsArrays.flat()) + } + + private getProcessTree( + parentProcess: string + ): Promise { + return this.getProcessPid(parentProcess).then(parentPid => { + if (parentPid == null) { + return null + } else { + return this.buildProcessTree(parentPid) + } + }) + } + + private getProcessPid(serviceName: string): Promise { + return new Promise((resolve, reject) => { + exec(`systemctl show ${serviceName} -p MainPID`, (error, stdout) => { + if (error != null) { + reject( + new Error(`Failed to get PID for ${serviceName}: ${error.message}`) + ) + } else { + const match = stdout.match(/MainPID=(\d+)/) + + if (match == null) { + resolve(null) + } else { + const pid = parseInt(match[1], 10) + resolve(pid > 1 ? pid : null) + } + } + }) + }) + } + + // Recursively build the process tree, scraping the cmdline string for each pid. + private buildProcessTree(pid: number): Promise { + return Promise.all([ + this.getProcessCmdline(pid), + this.getChildProcessesFrom(pid), + ]).then(([cmd, childPids]) => { + return Promise.all( + childPids.map(childPid => this.buildProcessTree(childPid)) + ).then(children => ({ + pid, + cmd, + children, + })) + }) + } + + // Get the exact cmdline string for the given pid, truncating if necessary. + private getProcessCmdline(pid: number): Promise { + return fs + .readFile(path.join(this.procPath, String(pid), 'cmdline'), 'utf8') + .then(cmdline => { + const cmd = cmdline.replace(/\0/g, ' ').trim() + return cmd.length > MAX_CMD_STR_LENGTH + ? `${cmd.substring(0, MAX_CMD_STR_LENGTH)}...` + : cmd + }) + .catch(error => { + this.log.error(`Failed to read cmdline for PID ${pid}`, error) + return `PID ${pid}` + }) + } + + private getChildProcessesFrom(parentPid: number): Promise { + return new Promise((resolve, reject) => { + exec(`pgrep -P ${parentPid}`, (error, stdout) => { + // code 1 means no children found + if (error != null && error.code !== 1) { + reject(error) + } else { + const children = stdout + .trim() + .split('\n') + .filter(line => line.length > 0) + .map(pid => parseInt(pid, 10)) + + resolve(children) + } + }) + }) + } + + // Get the actual metric(s) for a given node and recursively get metric(s) for all child nodes. + private getProcessDetailsFlattened( + node: ProcessTreeNode + ): Promise { + return this.getProcessMemory(node.pid).then(memRssMb => { + const currentNodeDetails: ProcessDetails = { + name: node.cmd, + memRssMb, + } + + return Promise.all( + node.children.map(child => this.getProcessDetailsFlattened(child)) + ).then(childDetailsArrays => { + return [currentNodeDetails, ...childDetailsArrays.flat()] + }) + }) + } + + // Scrape VmRSS from /proc/pid/status for a given pid. + private getProcessMemory(pid: number): Promise { + return fs + .readFile(path.join(this.procPath, String(pid), 'status'), 'utf8') + .then(status => { + const match = status.match(/VmRSS:\s+(\d+)\s+kB/) + if (match == null) { + throw new Error('Could not find VmRSS in status file') + } else { + const memInKb = parseInt(match[1], 10) + return (memInKb / 1024).toFixed(2) + } + }) + .catch(error => { + throw new Error( + `Failed to read memory info for PID ${pid}: ${error.message}` + ) + }) + } +} diff --git a/app-shell-odd/src/monitor/__tests__/ResourceMonitor.test.ts b/app-shell-odd/src/monitor/__tests__/ResourceMonitor.test.ts new file mode 100644 index 00000000000..416ddd15204 --- /dev/null +++ b/app-shell-odd/src/monitor/__tests__/ResourceMonitor.test.ts @@ -0,0 +1,162 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck -- Get around private method access warnings. + +import path from 'path' +import fs from 'fs-extra' +import tempy from 'tempy' +import { + vi, + describe, + beforeEach, + afterEach, + afterAll, + it, + expect, +} from 'vitest' +import { exec } from 'child_process' + +import { ResourceMonitor, PARENT_PROCESSES } from '../ResourceMonitor' +import { UI_INITIALIZED } from '../../constants' + +vi.mock('child_process') +vi.mock('../../log', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + createLogger: () => ({ + debug: vi.fn(), + error: vi.fn(), + }), + } +}) + +describe('ResourceMonitor', () => { + let procDir: string + let monitor: ResourceMonitor + const tempDirs: string[] = [] + + beforeEach(async () => { + procDir = tempy.directory() + tempDirs.push(procDir) + + vi.mocked(exec).mockImplementation((cmd, callback) => { + if (cmd.startsWith('systemctl')) { + callback(null, 'MainPID=1234\n') + } else if (cmd.startsWith('pgrep')) { + callback({ code: 1 } as any, '') + } + return {} as any + }) + + // Populate mock files with some mock data. + await fs.writeFile(path.join(procDir, 'uptime'), '3600.00 7200.00\n') + await fs.writeFile( + path.join(procDir, 'meminfo'), + 'MemTotal: 8192000 kB\nMemAvailable: 4096000 kB\n' + ) + + const parentPidDir = path.join(procDir, '1234') + await fs.ensureDir(parentPidDir) + await fs.writeFile( + path.join(parentPidDir, 'cmdline'), + 'process1234\0arg1\0arg2' + ) + await fs.writeFile( + path.join(parentPidDir, 'status'), + 'Name:\tprocess\nVmRSS:\t2048 kB\n' + ) + + monitor = new ResourceMonitor({ procPath: procDir }) + }) + + afterEach(() => { + monitor.stop() + }) + + afterAll(() => { + vi.resetAllMocks() + return Promise.all(tempDirs.map(d => fs.remove(d))) + }) + + describe('getSystemUptimeHrs', () => { + it('reads and parses system uptime', () => { + return monitor.getResourceDetails().then(details => { + expect(details.systemUptimeHrs).toBe('1.00') + }) + }) + + it('handles error reading uptime file', async () => { + await fs.remove(path.join(procDir, 'uptime')) + await expect(monitor.getResourceDetails()).rejects.toThrow( + 'Failed to read system uptime' + ) + }) + }) + + describe('getSystemAvailableMemory', () => { + it('reads and parses available memory', () => { + return monitor.getResourceDetails().then(details => { + expect(details.systemAvailMemMb).toBe('4000.00') + }) + }) + + it('handles missing MemAvailable in meminfo', async () => { + await fs.writeFile( + path.join(procDir, 'meminfo'), + 'MemTotal: 8192000 kB\n' + ) + + await expect(monitor.getResourceDetails()).rejects.toThrow( + 'Could not find MemAvailable in meminfo file' + ) + }) + }) + + describe('getProcessDetails', () => { + it('collects process details for parent process', () => { + return monitor.getResourceDetails().then(details => { + expect(details.processesDetails).toHaveLength(PARENT_PROCESSES.length) + expect(details.processesDetails[0]).toEqual({ + name: 'process1234 arg1 arg2', + memRssMb: '2.00', + }) + }) + }) + + it('handles missing process', () => { + // Mock exec to return non-existent PID + vi.mocked(exec).mockImplementation((cmd, callback) => { + if (cmd.startsWith('systemctl')) { + callback(null, 'MainPID=9999\n') + } else { + callback({ code: 1 } as any, '') + } + return {} as any + }) + + return monitor.getResourceDetails().then(details => { + expect(details.processesDetails).toHaveLength(0) + }) + }) + + it('handles errors reading process details', async () => { + await fs.remove(path.join(procDir, '1234', 'status')) + await monitor.getResourceDetails().then(details => { + expect(details.processesDetails).toHaveLength(0) + }) + }) + }) + + describe('start', () => { + it(`handler correctly updates internal state when ${UI_INITIALIZED} is dispatched`, () => { + const dispatch = vi.fn() + const handler = monitor.start(dispatch) + + expect(typeof handler).toBe('function') + + handler({ type: UI_INITIALIZED }) + + expect(monitor.intervalId).not.toBeNull() + }) + }) +}) diff --git a/app-shell-odd/src/monitor/index.ts b/app-shell-odd/src/monitor/index.ts new file mode 100644 index 00000000000..8f257b19aa1 --- /dev/null +++ b/app-shell-odd/src/monitor/index.ts @@ -0,0 +1,8 @@ +import { ResourceMonitor } from './ResourceMonitor' + +import type { Dispatch } from '../types' + +export function registerResourceMonitor(dispatch: Dispatch): Dispatch { + const resourceMonitor = new ResourceMonitor() + return resourceMonitor.start(dispatch) +} diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 42335754432..7be1070ca12 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -22,6 +22,7 @@ import { MaintenanceRunTakeover } from '/app/organisms/TakeoverModal' import { FirmwareUpdateTakeover } from '/app/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover' import { IncompatibleModuleTakeover } from '/app/organisms/IncompatibleModule' import { EstopTakeover } from '/app/organisms/EmergencyStop' +import { ChooseLanguage } from '/app/pages/ODD/ChooseLanguage' import { ConnectViaEthernet } from '/app/pages/ODD/ConnectViaEthernet' import { ConnectViaUSB } from '/app/pages/ODD/ConnectViaUSB' import { ConnectViaWifi } from '/app/pages/ODD/ConnectViaWifi' @@ -66,6 +67,7 @@ import type { Dispatch } from '/app/redux/types' hackWindowNavigatorOnLine() export const ON_DEVICE_DISPLAY_PATHS = [ + '/choose-language', '/dashboard', '/deck-configuration', '/emergency-stop', @@ -94,6 +96,8 @@ function getPathComponent( path: typeof ON_DEVICE_DISPLAY_PATHS[number] ): JSX.Element { switch (path) { + case '/choose-language': + return case '/dashboard': return case '/deck-configuration': diff --git a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx index 662b2523436..2d2d09ca15e 100644 --- a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx +++ b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { LocalizationProvider } from '../../LocalizationProvider' +import { ChooseLanguage } from '/app/pages/ODD/ChooseLanguage' import { ConnectViaEthernet } from '/app/pages/ODD/ConnectViaEthernet' import { ConnectViaUSB } from '/app/pages/ODD/ConnectViaUSB' import { ConnectViaWifi } from '/app/pages/ODD/ConnectViaWifi' @@ -48,6 +49,7 @@ vi.mock('@opentrons/react-api-client', async () => { vi.mock('../../LocalizationProvider') vi.mock('/app/pages/ODD/Welcome') vi.mock('/app/pages/ODD/NetworkSetupMenu') +vi.mock('/app/pages/ODD/ChooseLanguage') vi.mock('/app/pages/ODD/ConnectViaEthernet') vi.mock('/app/pages/ODD/ConnectViaUSB') vi.mock('/app/pages/ODD/ConnectViaWifi') @@ -109,6 +111,10 @@ describe('OnDeviceDisplayApp', () => { vi.resetAllMocks() }) + it('renders ChooseLanguage component from /choose-language', () => { + render('/choose-language') + expect(vi.mocked(ChooseLanguage)).toHaveBeenCalled() + }) it('renders Welcome component from /welcome', () => { render('/welcome') expect(vi.mocked(Welcome)).toHaveBeenCalled() diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 5f6354da6c5..3f957ebe608 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -15,11 +15,14 @@ "additional_labware_folder_title": "Additional Custom Labware Source Folder", "advanced": "Advanced", "app_changes": "App Changes in ", + "app_language_description": "All app features use this language. Protocols and other user content will not change language.", + "app_language_preferences": "App Language Preferences", "app_settings": "App Settings", "bug_fixes": "Bug Fixes", "cal_block": "Always use calibration block to calibrate", "change_folder_button": "Change labware source folder", "channel": "Channel", + "choose_your_language": "Choose your language", "clear_confirm": "Clear unavailable robots", "clear_robots_button": "Clear unavailable robots list", "clear_robots_description": "Clear the list of unavailable robots on the Devices page. This action cannot be undone.", @@ -48,6 +51,7 @@ "ip_available": "Available", "ip_description_first": "Enter an IP address or hostname to connect to a robot.", "language_preference": "Language preference", + "language": "Language", "manage_versions": "It is very important for the robot and app software to be on the same version. Manage the robot software versions via Robot Settings > Advanced.", "new_features": "New Features", "no_folder": "No additional source folder specified", @@ -73,6 +77,7 @@ "restarting_app": "Download complete, restarting the app...", "restore_previous": "See how to restore a previous software version", "searching": "Searching for 30s", + "select_a_language": "Select a language to personalize your experience.", "select_language": "Select language", "setup_connection": "Set up connection", "share_display_usage": "Share display usage", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 5640f3306a5..6dbee9af16f 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -1,7 +1,7 @@ { - "absorbance_reader_open_lid": "Opening Absorbance Reader lid", "absorbance_reader_close_lid": "Closing Absorbance Reader lid", "absorbance_reader_initialize": "Initializing Absorbance Reader to perform {{mode}} measurement at {{wavelengths}}", + "absorbance_reader_open_lid": "Opening Absorbance Reader lid", "absorbance_reader_read": "Reading plate in Absorbance Reader", "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in {{slot}}", "adapter_in_slot": "{{adapter}} in {{slot}}", @@ -27,6 +27,7 @@ "dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL", "drop_tip": "Dropping tip in {{well_name}} of {{labware}}", "drop_tip_in_place": "Dropping tip in place", + "dropping_tip_in_trash": "Dropping tip in {{trash}}", "engaging_magnetic_module": "Engaging Magnetic Module", "fixed_trash": "Fixed Trash", "home_gantry": "Homing all gantry, pipette, and plunger axes", @@ -73,16 +74,16 @@ "setting_thermocycler_lid_temp": "Setting Thermocycler lid temperature to {{temp}}", "single": "single", "slot": "Slot {{slot_name}}", - "turning_rail_lights_off": "Turning rail lights off", - "turning_rail_lights_on": "Turning rail lights on", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", "tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}", - "tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:", "tc_starting_extended_profile": "Running thermocycler profile with {{elementCount}} total steps and cycles:", + "tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:", "tc_starting_profile": "Running thermocycler profile with {{stepCount}} steps:", "touch_tip": "Touching tip", "trash_bin_in_slot": "Trash Bin in {{slot_name}}", + "turning_rail_lights_off": "Turning rail lights off", + "turning_rail_lights_on": "Turning rail lights on", "unlatching_hs_latch": "Unlatching labware on Heater-Shaker", "wait_for_duration": "Pausing for {{seconds}} seconds. {{message}}", "wait_for_resume": "Pausing protocol", diff --git a/app/src/assets/localization/zh/anonymous.json b/app/src/assets/localization/zh/anonymous.json index 696dff65751..045245c84f7 100644 --- a/app/src/assets/localization/zh/anonymous.json +++ b/app/src/assets/localization/zh/anonymous.json @@ -2,6 +2,8 @@ "a_robot_software_update_is_available": "需要更新工作站软件版本才能使用该版本的桌面应用程序运行协议。转到工作站这个金属块是一个特制的工具,完美适配您的甲板,有助于校准。如果您没有校准块,请发送电子邮件给支持团队,以便我们寄送一个给您。在您提供的信息中,请确保包括您的姓名、公司或机构名称和寄送地址。在等待校准块到达过程中,您可以暂时利用工作站里垃圾桶上的平面进行校准。", "calibration_on_opentrons_tips_is_important": "使用上述吸头和吸头盒进行校准非常重要,因为工作站的准确性是基于这些吸头的已知尺寸来确定的。", "choose_what_data_to_share": "选择要共享的工作站数据。", @@ -15,6 +17,7 @@ "contact_support_for_connection_help": "如果以上方法都无法解决问题,请联系支持人员寻求帮助(通过此应用程序中的问号链接,或发送电子邮件至{{support_email}}。)", "deck_fixture_setup_modal_bottom_description": "有关安装不同类型固定装置的详细信息,请与支持人员联系。", "delete_protocol_from_app": "删除协议,针对错误进行修改,然后从桌面应用程序将协议重新发送到此工作站。", + "delete_transfer_from_app": "删除快速移液,修改并解决错误,在工作站显示屏幕上重新创建此移液。", "error_boundary_description": "您需要重新启动触摸屏。联系支持人员以获取帮助。", "estop_pressed_description": "首先,安全清理甲板上的任何实验耗材和洒出的液体。然后,顺时针旋转急停开关。最后,让工作站将龙门架移动到其原位。", "find_your_robot": "在应用程序的“设备”栏找到您的工作站,以安装软件更新。", @@ -34,6 +37,7 @@ "module_calibration_get_started": "开始前,请从工作台上移除实验耗材并清理工作区,以便于校准。还需准备好右侧显示的所需设备。校准适配器随模块一起提供。移液器探头随移液器一起提供。", "module_error_contact_support": "尝试关闭模块电源,然后再打开。如果报错仍然存在,请与支持人员联系。", "network_setup_menu_description": "您将使用此连接来运行软件更新,并将协议加载到您的工作站上。", + "new_robot_instructions": "设置新工作站时,请遵循触摸屏上的指示。有关更多信息,请参阅您的工作站快速入门指南。", "oem_mode_description": "启用OEM模式,以从Flex触摸屏中移除Opentrons的相关信息。", "opentrons_app_successfully_updated": "应用程序已成功更新。", "opentrons_app_update": "应用程序更新", @@ -42,6 +46,7 @@ "opentrons_app_will_use_interpreter": "在指定路径后,应用程序将使用此路径的Python解释器,而不是默认绑定的Python解释器。", "opentrons_cares_about_privacy": "我们注重您的隐私。我们匿名化所有数据,仅用于改进我们的产品。", "opentrons_def": "已验证的数据", + "opentrons_flex_quickstart_guide": "快速入门指南", "opentrons_labware_def": "已验证的实验耗材数据", "opentrons_tip_racks_recommended": "建议使用Opentrons吸头盒。其他吸头盒无法保证精度。", "opentrons_tip_rack_name": "opentrons", @@ -62,6 +67,7 @@ "share_logs_with_opentrons_description": "通过自动发送匿名的工作站日志来帮助改进此产品。这些日志用于解决工作站问题和发现错误趋势。", "show_labware_offset_snippets_description": "仅适用于需要在应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", "something_seems_wrong": "您的移液器可能有问题。退出设置并联系支持人员以获取帮助。", + "storage_limit_reached_description": "您的工作站已达到可存储的快速移液数量上限。在创建新的快速移液之前,您必须删除一个现有的快速移液。", "these_are_advanced_settings": "这些是高级设置。请勿在没有支持团队帮助的情况下尝试调整这些设置。更改这些设置可能会影响您的移液器寿命。这些设置不会覆盖协议中定义的任何移液器设置。", "update_requires_restarting_app": "更新需要重新启动应用程序。", "update_robot_software_description": "绕过自动更新过程并手动更新工作站软件", diff --git a/app/src/assets/localization/zh/app_settings.json b/app/src/assets/localization/zh/app_settings.json index 9e4a9f1bbc7..3405d5edbfd 100644 --- a/app/src/assets/localization/zh/app_settings.json +++ b/app/src/assets/localization/zh/app_settings.json @@ -1,10 +1,10 @@ { + "__dev_internal__enableLabwareCreator": "启用应用实验耗材创建器", + "__dev_internal__enableLocalization": "Enable App Localization", "__dev_internal__forceHttpPolling": "强制轮询所有网络请求,而不是使用MQTT", - "__dev_internal__protocolStats": "协议统计", "__dev_internal__enableRunNotes": "在协议运行期间显示备注", - "__dev_internal__enableQuickTransfer": "启用快速移液", - "__dev_internal__enableLabwareCreator": "Enable App Labware Creator", - "__dev_internal__enableLocalization": "Enable App Localization", + "__dev_internal__protocolStats": "协议统计", + "__dev_internal__protocolTimeline": "协议时间线", "add_folder_button": "添加实验耗材源文件夹", "add_ip_button": "添加", "add_ip_error": "输入IP地址或主机名", diff --git a/app/src/assets/localization/zh/branded.json b/app/src/assets/localization/zh/branded.json index 4bff5f976d4..c38888398f1 100644 --- a/app/src/assets/localization/zh/branded.json +++ b/app/src/assets/localization/zh/branded.json @@ -2,6 +2,8 @@ "a_robot_software_update_is_available": "需要更新工作站软件才能使用此版本的Opentrons应用程序运行协议。转到工作站", "about_flex_gripper": "关于Flex转板抓手", "alternative_security_types_description": "Opentrons应用程序支持将Flex连接到各种企业接入点。通过USB连接并在应用程序中完成设置。", + "attach_a_pipette_for_quick_transfer": "要创建快速移液,您需要将移液器安装到您的Opentrons Flex上。", + "attach_a_pipette": "将移液器连接到Flex", "calibration_block_description": "这个金属块是一个特制的工具,完美适配您的甲板,有助于校准。如果您没有校准块,请发送电子邮件至support@opentrons.com,以便我们寄送一个给您。在您提供的信息中,请确保包括您的姓名、公司或机构名称和寄送地址。在等待校准块到达过程中,您可以暂时利用工作站里垃圾桶上的平面进行校准。", "calibration_on_opentrons_tips_is_important": "使用上述Opentrons吸头和吸头盒进行校准非常重要,因为工作站的准确性是基于这些吸头的已知尺寸来确定的。", "choose_what_data_to_share": "选择要与Opentrons共享的数据。", @@ -15,6 +17,7 @@ "contact_support_for_connection_help": "如果以上方法都无法解决问题,请联系Opentrons支持人员寻求帮助(通过此应用程序中的问号链接,或发送电子邮件至{{support_email}}。)", "deck_fixture_setup_modal_bottom_description": "有关安装不同类型固定装置的详细信息,请扫描二维码或在support.opentrons.com上搜索“deck configuration”", "delete_protocol_from_app": "删除协议,针对错误进行修改,然后从Opentrons应用程序将协议重新发送到此工作站。", + "delete_transfer_from_app": "删除快速移液,修改并解决错误,在Flex显示屏幕上重新创建此移液。", "error_boundary_description": "您需要重新启动触摸屏。然后从Opentrons应用程序下载工作站日志并将其发送到support@opentrons.com寻求帮助。", "estop_pressed_description": "首先,安全清理甲板上的任何实验耗材或洒出液体。然后,顺时针旋转急停开关。最后,让Flex将龙门架移动到其原位。", "find_your_robot": "在Opentrons应用程序中找到您的工作站以安装软件更新。", @@ -34,6 +37,7 @@ "module_calibration_get_started": "开始前,请从工作台上移除实验耗材并清理工作区,以便于校准。还需准备好右侧显示的所需设备。校准适配器随模块一起提供。移液器探头随Flex移液器一起提供。", "module_error_contact_support": "尝试关闭模块电源,然后再打开。如果报错仍然存在,请与Opentrons支持人员联系。", "network_setup_menu_description": "您将使用此连接来运行软件更新,并将协议加载到您的Opentrons Flex上。", + "new_robot_instructions": "设置新的Flex时,请遵循触摸屏上的指示。有关更多信息,请参阅您的工作站快速入门指南。", "oem_mode_description": "启用OEM模式,以从Flex触摸屏中移除Opentrons的所有信息。", "opentrons_app_successfully_updated": "Opentrons应用程序已成功更新。.", "opentrons_app_update": "Opentrons应用程序更新", @@ -42,6 +46,7 @@ "opentrons_app_will_use_interpreter": "如果指定,Opentrons应用程序将在此路径使用Python解释器,而不是默认绑定的Python解释器。", "opentrons_cares_about_privacy": "Opentrons关心您的隐私。我们匿名化所有数据,仅用于改进我们的产品。", "opentrons_def": "已验证的Opentrons数据", + "opentrons_flex_quickstart_guide": "Opentrons Flex 快速入门指南", "opentrons_labware_def": "已验证的Opentrons实验耗材数据", "opentrons_tip_rack_name": "opentrons", "opentrons_tip_racks_recommended": "建议使用Opentrons吸头盒。其他吸头盒无法保证精度。", @@ -62,6 +67,7 @@ "share_logs_with_opentrons_description": "通过自动发送匿名的工作站日志,帮助Opentrons改进产品和服务。Opentrons使用这些日志来解决工作站问题并发现错误趋势。", "show_labware_offset_snippets_description": "仅适用于需要在Opentrons应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", "something_seems_wrong": "您的移液器可能有问题。退出设置并联系Opentrons支持人员以获取帮助。", + "storage_limit_reached_description": "您的 Opentrons Flex 已达到可存储的快速移液数量上限。在创建新的快速移液之前,您必须删除一个现有的快速移液。", "these_are_advanced_settings": "这些是高级设置。请勿在没有Opentrons支持团队帮助的情况下尝试调整这些设置。更改这些设置可能会影响您的移液器寿命。这些设置不会覆盖协议中定义的任何移液器设置。", "update_requires_restarting_app": "更新需要重新启动Opentrons应用程序。", "update_robot_software_description": "绕过Opentrons应用程序自动更新过程并手动更新工作站软件", diff --git a/app/src/assets/localization/zh/device_details.json b/app/src/assets/localization/zh/device_details.json index 39115321f99..a19e61a365b 100644 --- a/app/src/assets/localization/zh/device_details.json +++ b/app/src/assets/localization/zh/device_details.json @@ -3,6 +3,7 @@ "about_module": "关于{{name}}", "about_pipette_name": "关于{{name}}移液器", "about_pipette": "关于移液器", + "abs_reader_status": "吸光度读板器状态", "add_fixture_description": "将此硬件添加至甲板配置。它在协议分析期间将会被引用。", "add_to_slot": "添加到板位{{slotName}}", "add": "添加", @@ -137,6 +138,7 @@ "recalibrate_pipette_offset": "重新校准移液器偏移", "recalibrate_pipette": "重新校准移液器", "recent_protocol_runs": "最近的协议运行", + "rerun_loading": "数据完全加载前,禁止协议重新运行", "rerun_now": "立即重新运行协议", "reset_all": "重置全部", "reset_estop": "重置急停", diff --git a/app/src/assets/localization/zh/device_settings.json b/app/src/assets/localization/zh/device_settings.json index c69e9e46131..ecd81c941dd 100644 --- a/app/src/assets/localization/zh/device_settings.json +++ b/app/src/assets/localization/zh/device_settings.json @@ -319,6 +319,7 @@ "wpa2_personal": "WPA2个人", "wpa2_personal_description": "大多数实验室都使用此方法", "yes_clear_data_and_restart_robot": "是,清除数据并重新启动工作站", + "you_should_not_downgrade": "您不应降级到工作站制造日期之前的软件版本。", "your_mac_address_is": "您的MAC地址是{{macAddress}}", "your_robot_is_ready_to_go": "您的工作站已准备就绪。" } diff --git a/app/src/assets/localization/zh/devices_landing.json b/app/src/assets/localization/zh/devices_landing.json index 587959751e9..8e6af9d5ba9 100644 --- a/app/src/assets/localization/zh/devices_landing.json +++ b/app/src/assets/localization/zh/devices_landing.json @@ -8,8 +8,8 @@ "devices": "设备", "disconnect_from_network": "断开网络连接", "empty": "空", - "forget_unavailable_robot": "忘记无法使用的工作站", - "go_to_run": "去运行", + "forget_unavailable_robot": "删除无法使用的工作站", + "go_to_run": "返回运行界面", "home_gantry": "龙门架归位", "how_to_setup_a_robot": "如何设置新工作站", "idle": "空闲", @@ -28,6 +28,7 @@ "modules": "模块", "no_robots_found": "未找到工作站", "not_available": "不可用({{count}})", + "ot2_quickstart_guide": "OT-2 快速入门指南", "refresh": "刷新", "restart_the_app": "重启应用程序", "restart_the_robot": "重启工作站", diff --git a/app/src/assets/localization/zh/drop_tip_wizard.json b/app/src/assets/localization/zh/drop_tip_wizard.json index 8984387755e..a5ae8faebfc 100644 --- a/app/src/assets/localization/zh/drop_tip_wizard.json +++ b/app/src/assets/localization/zh/drop_tip_wizard.json @@ -5,22 +5,31 @@ "blowout_liquid": "吹出液体", "cant_safely_drop_tips": "无法安全丢弃吸头", "choose_blowout_location": "选择吹液位置", + "choose_deck_location": "选择甲板位置", "choose_drop_tip_location": "选择吸头丢弃位置", "confirm_blowout_location": "移液器是否位于应吹出液体的位置?", "confirm_drop_tip_location": "移液器是否位于应丢弃吸头的位置?", + "confirm_position": "确认位置", "confirm_removal_and_home": "确认移除并回到原点", + "continue": "继续", "drop_tip_complete": "吸头丢弃完成", "drop_tip_failed": "丢弃吸头操作未能完成,请联系技术支持获取帮助。", "drop_tips": "丢弃吸头", "error_dropping_tips": "丢弃吸头时发生错误", + "exit_and_home_pipette": "退出并归位移液器", "exit_screen_title": "在完成吸头丢弃前退出?", + "exit": "退出", "getting_ready": "正在准备…", "go_back": "返回", + "jog_too_far": "移动过远?", + "liquid_damages_pipette": "如果移液器中有液体,将移液器归位可能会损坏它。您必须在使用移液器之前取下所有吸头。", + "liquid_damages_this_pipette": "如果{{mount}}移液器的移液器中有液体,将其归位可能会损坏它。您必须在使用移液器之前取下所有吸头。", "move_to_slot": "移至板位", "no_proceed_to_drop_tip": "否,继续进行吸头移除", "position_and_blowout": "确保移液器吸头尖端位于指定位置的正上方并保持水平。如果不是,请使用下面的控制键或键盘微调移液器直到正确位置。", "position_and_drop_tip": "确保移液器吸头尖端位于指定位置的正上方并保持水平。如果不是,请使用下面的控制键或键盘微调移液器直到正确位置。", "position_the_pipette": "调整移液器位置", + "remove_any_attached_tips": "移除任何已安装的吸头", "remove_the_tips": "在协议中再次使用前,您可能需要从{{mount}}移液器上移除吸头。", "remove_the_tips_from_pipette": "在协议中再次使用前,您可能需要从移液器上移除吸头。", "remove_the_tips_manually": "手动移除吸头,然后使龙门架回原点。在拾取吸头的状态下归位可能导致移液器吸入液体并损坏。", @@ -29,11 +38,15 @@ "select_blowout_slot_odd": "您可以将液体吹入耗材中。
龙门架移动到选定的板位后,使用位置控制按键将移液器移动到吹出液体的确切位置。", "select_drop_tip_slot": "您可以将吸头返回吸头架或丢弃它们。在右侧的甲板图上选择您想丢弃吸头的板位。确认后龙门架将移动到选定的板位。", "select_drop_tip_slot_odd": "您可以将吸头放回吸头架或丢弃它们。
龙门架移动到选定的板位后,使用位置控制按键将移液器移动到丢弃吸头的确切位置。", + "skip_and_home_pipette": "跳过并归位移液器", "skip": "跳过", "stand_back_blowing_out": "请远离,工作站正在吹出液体", "stand_back_dropping_tips": "请远离,工作站正在丢弃吸头", "stand_back_robot_in_motion": "请远离,工作站正在移动", - "tips_are_attached": "吸头已拾取", - "tips_may_be_attached": "可能已拾取吸头。", + "start_over": "重新开始", + "trash_bin_in_slot": "垃圾桶在 {{slot}}", + "waste_chute_in_slot": "外置垃圾槽在 {{slot}}", + "where_to_blowout": "您希望在哪里吹出液体?", + "where_to_drop_tips": "您希望在哪里丢弃吸头?", "yes_blow_out_liquid": "是的,将液体吹入耗材中" } diff --git a/app/src/assets/localization/zh/error_recovery.json b/app/src/assets/localization/zh/error_recovery.json index 2383ca8f850..dddf7923d4b 100644 --- a/app/src/assets/localization/zh/error_recovery.json +++ b/app/src/assets/localization/zh/error_recovery.json @@ -1,4 +1,5 @@ { + "another_app_controlling_robot": "工作站的触摸屏或另一台装有应用程序的电脑正在控制这个工作站。", "are_you_sure_you_want_to_cancel": "您确定要取消吗?", "at_step": "在步骤", "back_to_menu": "返回菜单", @@ -11,25 +12,44 @@ "change_location": "更改位置", "change_tip_pickup_location": "更换拾取吸头的位置", "choose_a_recovery_action": "选择恢复操作", + "close_door_to_resume": "关闭工作站门以继续", + "close_robot_door": "关闭移液工作站前门", + "close_the_robot_door": "关闭工作站门,然后继续恢复操作。", "confirm": "确认", "continue": "继续", "continue_run_now": "现在继续运行", "continue_to_drop_tip": "继续丢弃吸头", + "ensure_lw_is_accurately_placed": "确保实验耗材已准确放置在甲板槽中,防止进一步出现错误", + "error_details": "错误详情", + "error_on_robot": "{{robot}}上的错误", "error": "错误", "failed_dispense_step_not_completed": "中断运行的最后一步液体排出失败,恢复程序将不会继续运行这一步骤,请手动完成这一步的移液操作。运行将继续从下一步开始。继续之前,请关闭工作站门。", "failed_step": "失败步骤", + "first_is_gripper_holding_labware": "首先,抓扳手是否夹着实验耗材?", "go_back": "返回", + "gripper_error": "抓扳手错误", + "gripper_errors_occur_when": "当抓扳手停滞或与甲板上另一物体碰撞时,会发生抓扳手错误,这通常是由于实验耗材放置不当或实验耗材偏移不准确所致", + "gripper_releasing_labware": "抓扳手正在释放实验耗材", + "gripper_will_release_in_s": "抓扳手将在{{seconds}}秒后释放实验耗材", + "homing_pipette_dangerous": "如果移液器中有液体,将{{mount}}移液器归位可能会损坏它。您必须在使用移液器之前取下所有吸头。", + "if_issue_persists_gripper_error": "如果问题持续存在,请取消运行并重新运行抓扳手校准", + "if_issue_persists_overpressure": "如果问题持续存在,请取消运行并对协议进行必要的更改", + "if_issue_persists_tip_not_detected": "如果问题持续存在,请取消运行并启动实验耗材位置检查", "if_tips_are_attached": "如果吸头还在移液器上,您可以在运行终止前选择吹出已吸取的液体并丢弃吸头。", "ignore_all_errors_of_this_type": "忽略所有此类错误", "ignore_error_and_skip": "忽略错误并跳到下一步", - "skipping_to_step_succeeded": "跳转到步骤{{step}}成功", - "retrying_step_succeeded": "重试步骤{{step}}成功", "ignore_only_this_error": "仅忽略此错误", "ignore_similar_errors_later_in_run": "要在后续的运行中忽略类似错误吗?", + "labware_released_from_current_height": "将从当前高度释放实验耗材", "launch_recovery_mode": "启动恢复模式", "manually_fill_liquid_in_well": "手动填充孔位{{well}}中的液体", "manually_fill_well_and_skip": "手动填充孔位并跳到下一步", + "manually_move_lw_and_skip": "手动移动实验耗材并跳至下一步", + "manually_move_lw_on_deck": "手动移动实验耗材", + "manually_replace_lw_and_retry": "手动更换实验耗材并重试此步骤", + "manually_replace_lw_on_deck": "手动更换实验耗材", "next_step": "下一步", + "next_try_another_action": "接下来,您可以尝试另一个恢复操作或取消运行。", "no_liquid_detected": "未检测到液体", "pick_up_tips": "取吸头", "pipette_overpressure": "移液器超压", @@ -39,23 +59,32 @@ "recovery_action_failed": "{{action}}失败", "recovery_mode": "恢复模式", "recovery_mode_explanation": "恢复模式为您提供运行错误后的手动处理引导。
您可以进行调整以确保发生错误时正在进行的步骤可以完成,或者选择取消协议。当做出调整且未检测到后续错误时,该模式操作完成。根据导致错误的条件,系统将提供相应的调整选项。", + "release_labware_from_gripper": "从抓板手中释放实验耗材", + "release": "释放", + "remove_any_attached_tips": "移除任何已安装的吸头", "replace_tips_and_select_location": "建议更换吸头并选择最后一次取吸头的位置。", "replace_used_tips_in_rack_location": "在吸头板位{{location}}更换已使用的吸头", "replace_with_new_tip_rack": "更换新的吸头盒", - "first_take_any_necessary_actions": "首先,采取必要的准备操作,工作站将从失败的步骤开始继续运行。然后,在工作站继续运行之前关闭前门。", + "resume": "继续", + "retrying_step_succeeded": "重试步骤{{step}}成功", "retry_now": "现在重试", "retry_step": "重试步骤", "retry_with_new_tips": "使用新吸头重试", "retry_with_same_tips": "使用相同吸头重试", "return_to_menu": "返回菜单", - "return_to_the_menu": "返回菜单以选择如何继续。", + "robot_door_is_open": "工作站前门已打开", + "robot_is_canceling_run": "工作站正在取消运行", + "robot_is_in_recovery_mode": "工作站正在恢复模式", + "robot_not_attempt_to_move_lw": "工作站将不再尝试移动实验耗材。运行将从下一步继续。在继续之前,请关闭工作站门。", + "robot_retry_failed_lw_movement": "工作站将会从更换耗材的位置重新尝试失败的移液步骤。在继续之前,请关闭工作站门。", "robot_will_not_check_for_liquid": "工作站将不再检查液体。运行将从下一步继续。继续前请关闭工作站前门。", "robot_will_retry_with_new_tips": "工作站将使用新吸头重试失败的步骤。继续前请关闭工作站前门。", "robot_will_retry_with_same_tips": "工作站将使用相同的吸头重试失败的步骤。继续前请关闭工作站前门。", "robot_will_retry_with_tips": "工作站将使用新吸头重试失败的步骤。", "run_paused": "运行暂停", "select_tip_pickup_location": "选择取吸头位置", - "skip": "跳过", + "skipping_to_step_succeeded": "跳转到步骤{{step}}成功", + "skip_and_home_pipette": "跳过并归位移液器", "skip_to_next_step": "跳到下一步", "skip_to_next_step_new_tips": "使用新吸头跳到下一步", "skip_to_next_step_same_tips": "使用相同吸头跳到下一步", @@ -64,8 +93,13 @@ "stand_back_resuming": "请远离,正在恢复当前步骤", "stand_back_retrying": "请远离,正在重试失败步骤", "stand_back_skipping_to_next_step": "请远离,正在跳到下一步骤", + "take_any_necessary_precautions": "在接住实验耗材之前,请采取必要的预防措施。确认后,夹爪将开始倒计时再释放。", + "take_necessary_actions_failed_pickup": "首先,采取任何必要的行动,让工作站重新尝试移液器拾取。然后,在继续之前关闭工作站门。", + "take_necessary_actions": "首先,采取任何必要的行动,让工作站重新尝试失败的步骤。然后,在继续之前关闭工作站门。", + "terminate_remote_activity": "终止远程活动", "tip_drop_failed": "丢弃吸头失败", "tip_not_detected": "未检测到吸头", + "tip_presence_errors_are_caused": "吸头识别错误通常是由实验器皿放置不当或器皿偏移不准确引起的。", "view_error_details": "查看错误详情", "view_recovery_options": "查看恢复选项", "you_can_still_drop_tips": "在继续选择吸头之前,您仍然可以丢弃移液器上现存的吸头。", diff --git a/app/src/assets/localization/zh/labware_position_check.json b/app/src/assets/localization/zh/labware_position_check.json index fb92623de2e..cb27f78e66c 100644 --- a/app/src/assets/localization/zh/labware_position_check.json +++ b/app/src/assets/localization/zh/labware_position_check.json @@ -95,7 +95,7 @@ "return_tip_section": "放回吸头", "returning_tip_title": "正在板位{{slot}}放回吸头", "reveal_jog_controls": "显示调整面板", - "robot_has_no_offsets_from_previous_runs": "耗材校准数据引用自之前运行的协议,以节省您的时间。如果本协议中的所有耗材已在之前的运行中检查过,这些数据将应用于本次运行。 您可以使用耗材位置校准程序的后续步骤来添加耗材校准数据。", + "robot_has_no_offsets_from_previous_runs": "耗材校准数据引用自之前运行的协议,以节省您的时间。如果本协议中的所有耗材已在之前的运行中检查过,这些数据将应用于本次运行。 您可以在后面的步骤中使用耗材位置校准添加新的偏移量。", "robot_has_offsets_from_previous_runs": "此工作站具有本协议中所用耗材的校准数据。如果您应用了这些校准数据,仍可通过耗材位置校准程序进行调整。", "robot_in_motion": "工作站正在运行,请远离。", "run_labware_position_check": "运行耗材位置校准程序", diff --git a/app/src/assets/localization/zh/protocol_command_text.json b/app/src/assets/localization/zh/protocol_command_text.json index 81fff4fd220..74ab15b69b7 100644 --- a/app/src/assets/localization/zh/protocol_command_text.json +++ b/app/src/assets/localization/zh/protocol_command_text.json @@ -16,6 +16,7 @@ "deactivating_tc_block": "停用热循环仪温控功能", "deactivating_tc_lid": "停用热循环仪热盖功能", "degrees_c": "{{temp}}°C", + "detect_liquid_presence": "正在检测{{labware}}在{{labware_location}}中的{{well_name}}孔中的液体存在", "disengaging_magnetic_module": "下降磁力架模块", "dispense_push_out": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中,并推出{{push_out_volume}}µL", "dispense": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中", @@ -26,6 +27,16 @@ "fixed_trash": "垃圾桶", "home_gantry": "复位所有龙门架、移液器和柱塞轴", "latching_hs_latch": "在热震荡模块上锁定实验耗材", + "left": "左", + "load_labware_info_protocol_setup_adapter_module": "在{{module_name}}的甲板槽{{slot_name}}上加载适配器{{adapter_name}}中的{{labware}}", + "load_labware_info_protocol_setup_adapter_off_deck": "在板外加载适配器{{adapter_name}}中的{{labware}}", + "load_labware_info_protocol_setup_adapter": "在甲板槽{{slot_name}}上加载适配器{{adapter_name}}中的{{labware}}", + "load_labware_info_protocol_setup_no_module": "在甲板槽{{slot_name}}中加载{{labware}}", + "load_labware_info_protocol_setup_off_deck": "在板外加载{{labware}}", + "load_labware_info_protocol_setup": "在{{module_name}}的甲板槽{{slot_name}}中加载{{labware}}", + "load_liquids_info_protocol_setup": "将{{liquid}}加载到{{labware}}中", + "load_module_protocol_setup": "在甲板槽{{slot_name}}中加载模块{{module}}", + "load_pipette_protocol_setup": "在{{mount_name}}支架上加载{{pipette_name}}", "module_in_slot_plural": "{{module}}", "module_in_slot": "{{module}}在{{slot_name}}号板位", "move_labware_manually": "手动将{{labware}}从{{old_location}}移动到{{new_location}}", @@ -46,7 +57,9 @@ "pause": "暂停", "pickup_tip": "从{{labware_location}}的{{labware}}的{{well_range}}孔中拾取吸头", "prepare_to_aspirate": "准备使用{{pipette}}吸液", + "reloading_labware": "正在重新加载{{labware}}", "return_tip": "将吸头返回到{{labware_location}}的{{labware}}的{{well_name}}孔中", + "right": "右", "save_position": "保存位置", "set_and_await_hs_shake": "设置热震荡模块以{{rpm}}rpm 震动并等待达到该转速", "setting_hs_temp": "将热震荡模块的目标温度设置为{{temp}}", @@ -60,6 +73,8 @@ "tc_starting_profile": "热循环仪开始进行由以下步骤组成的{{repetitions}}次循环:", "trash_bin_in_slot": "垃圾桶在{{slot_name}}", "touch_tip": "吸头接触内壁", + "turning_rail_lights_off": "正在关闭导轨灯", + "turning_rail_lights_on": "正在打开导轨灯", "unlatching_hs_latch": "解锁热震荡模块上的实验耗材", "wait_for_duration": "暂停{{seconds}}秒。{{message}}", "wait_for_resume": "暂停协议", diff --git a/app/src/assets/localization/zh/protocol_details.json b/app/src/assets/localization/zh/protocol_details.json index b62bc2bb89a..22f4c0566fc 100644 --- a/app/src/assets/localization/zh/protocol_details.json +++ b/app/src/assets/localization/zh/protocol_details.json @@ -4,7 +4,7 @@ "both_mounts": "两个移液器支架", "choices": "{{count}}个选择", "choose_file": "选择文件", - "choose_robot_to_run": "选择要运行的工作站\n{{protocol_name}}", + "choose_robot_to_run": "选择要运行{{protocol_name}}的工作站", "clear_and_proceed_to_setup": "清除并继续设置", "connect_modules_to_see_controls": "连接模块以查看控制", "connected": "已连接", @@ -21,6 +21,8 @@ "description": "描述", "extension_mount": "扩展安装支架", "file_required": "需要文件", + "go_to_labware_definition": "转到实验耗材定义", + "go_to_timeline": "转到时间线", "gripper_pick_up_count_description": "使用转板抓手移动单个耗材的指令。", "gripper_pick_up_count": "转板次数", "hardware": "硬件", @@ -28,6 +30,7 @@ "labware": "耗材", "last_analyzed": "上一次分析", "last_updated": "上一次更新", + "left_and_right_mounts": "左+右安装架", "left_mount": "左移液器安装位", "left_right": "左,右", "liquid_name": "液体名称", @@ -40,6 +43,7 @@ "name": "名称", "no_available_robots_found": "未找到可用的工作站", "no_custom_values": "未指定任何自定义值", + "no_labware_specified": "此协议中未指定实验耗材", "no_parameters": "该协议未指定任何参数", "no_summary": "没有为此协议指定摘要。", "not_connected": "未连接", diff --git a/app/src/assets/localization/zh/protocol_info.json b/app/src/assets/localization/zh/protocol_info.json index e279383c495..073b4fd6319 100644 --- a/app/src/assets/localization/zh/protocol_info.json +++ b/app/src/assets/localization/zh/protocol_info.json @@ -25,6 +25,7 @@ "import_a_file": "导入协议以开始", "import_new_protocol": "导入协议", "import": "导入", + "incompatible_file_type": "不兼容的文件类型", "instrument_cal_data_title": "校准数据", "instrument_not_attached": "未连接", "instruments_title": "所需移液器", @@ -44,12 +45,12 @@ "launch_protocol_designer": "打开在线协议编辑器", "manual_steps_learn_more": "了解更多关于手动步骤的信息", "modules_title": "所需模块", - "most_recent_updates": "最新更新", + "most_recent_updates": "按时间排序", "no_history": "无运行历史", "no_labware_offset_data": "无耗材校准数据", "no_protocol_yet": "还没有协议?", "nothing_here_yet": "没有可显示的协议!", - "oldest_updates": "最早的更新", + "oldest_updates": "按时间倒序排序", "open_a_protocol": "打开一个协议以开始", "open_api_docs": "打开Python API文档", "organization_and_author": "组织/作者", @@ -64,10 +65,10 @@ "protocol_title": "协议 -{{protocol_name}}", "protocol_upload_failed": "协议上传失败。请修复错误后重试", "protocols": "协议", - "quick_transfer": "快速移液", "required_cal_data_title": "校准数据", "required_quantity_title": "数量", "required_type_title": "类型", + "requires_csv": "需要CSV", "robot_name_last_run": "{{robot_name}}的上次运行", "robot_type_first": "{{robotType}}协议优先", "run_again": "再次运行", diff --git a/app/src/assets/localization/zh/protocol_setup.json b/app/src/assets/localization/zh/protocol_setup.json index 7e5007e8a2c..40a5c8c00f9 100644 --- a/app/src/assets/localization/zh/protocol_setup.json +++ b/app/src/assets/localization/zh/protocol_setup.json @@ -8,6 +8,10 @@ "add_to_slot": "添加到{{slotName}}号板位", "additional_labware": "{{count}}个额外的耗材", "additional_off_deck_labware": "额外的甲板外耗材", + "all_files_associated": "与协议运行相关文件的所有详细信息均可在工作站屏幕上获得。", + "applied_labware_offset_data": "已应用的实验耗材偏移数据", + "applied_labware_offsets": "已应用的实验耗材偏移", + "are_you_sure_you_want_to_proceed": "您确定要继续运行吗?", "attach_gripper_failure_reason": "连接所需的转板抓手以继续", "attach_gripper": "连接转板抓手", "attach_module": "校准前连接模块", @@ -39,6 +43,7 @@ "calibration_status": "校准状态", "calibration": "校准", "cancel_and_restart_to_edit": "取消运行并重新启动设置以进行编辑", + "choose_csv_file": "选择CSV文件", "choose_enum": "选择{{displayName}}", "closing": "关闭中...", "complete_setup_before_proceeding": "完成设置后继续运行", @@ -46,12 +51,19 @@ "configured": "已配置", "confirm_heater_shaker_module_modal_description": "在开始运行之前,应使模块的两个锚固件完全伸出,以确保牢固连接。导热适配器应连接到模块上。", "confirm_heater_shaker_module_modal_title": "确认已连接热震荡模块", + "confirm_liquids": "确认液体", + "confirm_locations_and_volumes": "确认位置和体积", + "confirm_offsets": "确认偏移校准数据", + "confirm_placements": "确认放置位置", + "confirm_selection": "确认选择", "confirm_values": "确认这些值", "connect_all_hardware": "首先连接并校准所有硬件", "connect_all_mod": "首先连接所有模块", "connect_modules_for_controls": "连接模块以查看控制", "connection_info_not_available": "一旦运行开始,连接信息不可用", "connection_status": "连接状态", + "csv_files_on_robot": "工作站上的CSV文件", + "csv_files_on_usb": "USB上的CSV文件", "csv_file": "CSV 文件", "currently_configured": "当前已配置", "currently_unavailable": "当前不可用", @@ -63,9 +75,11 @@ "deck_conflict_info_thermocycler": "通过移除位置 A1 和 B1 中的固定装置来更新甲板配置。从甲板配置中移除对应装置或更新协议。", "deck_conflict_info": "通过移除位置 {{cutout}} 中的 {{currentFixture}} 来更新甲板配置。从甲板配置中移除对应装置或更新协议。", "deck_conflict": "甲板位置冲突", + "deck_hardware_ready": "甲板硬件准备", "deck_hardware": "甲板硬件", "deck_map": "甲板布局图", "default_values": "默认值", + "download_files": "下载文件", "example": "示例", "exit_to_deck_configuration": "退出到甲板配置", "extension_mount": "扩展安装支架", @@ -81,6 +95,7 @@ "heater_shaker_extra_attention": "使用闩锁控制,便于放置耗材。", "heater_shaker_labware_list_view": "要添加耗材,请使用切换键来控制闩锁", "how_offset_data_works": "耗材校准数据如何工作", + "individiual_well_volume": "单个孔体积", "initial_liquids_num_plural": "{{count}}种初始液体", "initial_liquids_num": "{{count}}种初始液体", "initial_location": "初始位置", @@ -96,6 +111,7 @@ "labware_latch": "耗材闩锁", "labware_location": "耗材位置", "labware_name": "耗材名称", + "labware_placement": "实验耗材放置", "labware_position_check_not_available_analyzing_on_robot": "在工作站上分析协议时,耗材位置校准不可用", "labware_position_check_not_available_empty_protocol": "耗材位置校准需要协议加载耗材和移液器", "labware_position_check_not_available": "运行开始后,耗材位置校准不可用", @@ -111,10 +127,14 @@ "learn_more_about_offset_data": "了解更多关于耗材校准数据的信息", "learn_more_about_robot_cal_link": "了解更多关于工作站校准的信息", "learn_more": "了解更多", + "liquid_information": "液体信息", + "liquid_name": "液体名称", "liquid_setup_step_description": "查看液体的起始位置和体积", "liquid_setup_step_title": "液体", + "liquids_confirmed": "液体已确认", "liquids_not_in_setup": "此协议未使用液体", "liquids_not_in_the_protocol": "此协议未指定液体。", + "liquids_ready": "液体准备", "liquids": "液体", "list_view": "列表视图", "loading_data": "加载数据...", @@ -141,6 +161,7 @@ "module_mismatch_body": "检查连接到该工作站的模块型号是否正确", "module_name": "模块", "module_not_connected": "未连接", + "module_setup_step_ready": "校准准备", "module_setup_step_title": "甲板硬件", "module_slot_location": "{{slotName}}号板位,{{moduleName}}", "module": "模块", @@ -184,6 +205,8 @@ "offset_data": "偏移校准数据", "offsets_applied_plural": "应用了{{count}}个偏移校准数据", "offsets_applied": "应用了{{count}}个偏移校准数据", + "offsets_confirmed": "偏移校准数据已确认", + "offsets_ready": "偏移校准数据准备", "on_adapter_in_mod": "在{{moduleName}}中的{{adapterName}}上", "on_adapter": "在{{adapterName}}上", "on_deck": "在甲板上", @@ -198,6 +221,8 @@ "pipette_offset_cal_description_bullet_3": "对用于校准移液器的吸头执行吸头长度校准后,重新进行移液器偏移校准。", "pipette_offset_cal_description": "这会测量移液器相对于移液器安装支架和甲板的X、Y和Z值。移液器偏移校准依赖于甲板校准和吸头长度校准。 ", "pipette_offset_cal": "移液器偏移校准", + "placements_confirmed": "位置已确认", + "placements_ready": "位置准备", "placement": "放置", "plug_in_module_to_configure": "插入{{module}}以将其添加到板位", "plug_in_required_module_plural": "插入并启动所需模块以继续", @@ -210,10 +235,12 @@ "proceed_to_run": "继续运行", "protocol_analysis_failed": "协议分析失败", "protocol_can_be_closed": "该协议现在可以关闭。", + "protocol_requires_csv": "此协议需要一个CSV文件。点击下面的CSV文件进行选择。", "protocol_run_canceled": "协议运行已取消。", "protocol_run_complete": "协议运行完成。", "protocol_run_failed": "协议运行失败。", "protocol_run_started": "协议运行已开始。", + "protocol_run_stopped": "协议运行已停止。", "protocol_specifies": "协议指定", "protocol_upload_revamp_feedback": "对此体验有反馈吗?", "quantity": "数量", @@ -237,10 +264,12 @@ "robot_cal_help_title": "工作站校准的工作原理", "robot_calibration_step_description_pipettes_only": "查看该协议所需的硬件和校准。", "robot_calibration_step_description": "查看该协议所需的移液器和吸头长度校准。", + "robot_calibration_step_ready": "校准准备", "robot_calibration_step_title": "硬件", "run_disabled_calibration_not_complete": "确保工作站校准完成后再继续运行", "run_disabled_modules_and_calibration_not_complete": "确保工作站校准完成并且所有模块已连接后再继续运行", "run_disabled_modules_not_connected": "确保所有模块已连接后再继续运行", + "run_labware_position_check_to_get_offsets": "运行实验室位置检查以获取实验室偏移数据。", "run_labware_position_check": "运行耗材位置校准", "run_never_started": "运行未开始", "run": "运行", @@ -252,6 +281,8 @@ "setup_is_view_only": "运行开始后设置仅供查看", "slot_location": "{{slotName}}号板位", "slot_number": "板位编号", + "stacked_slot": "堆叠槽", + "start_run": "开始运行", "status": "状态", "step": "步骤{{index}}", "there_are_no_unconfigured_modules": "没有连接{{module}}。请连接一个模块并放置在{{slot}}号板位中。", @@ -260,20 +291,25 @@ "tip_length_cal_description": "这将测量吸头底部与移液器喷嘴之间的Z轴距离。如果对用于校准移液器的吸头重新进行吸头长度校准,也需要重新进行移液器偏移校准。", "tip_length_cal_title": "吸头长度校准", "tip_length_calibration": "吸头长度校准", + "total_liquid_volume": "总体积", "update_deck_config": "更新甲板配置", "update_deck": "更新甲板", + "update_offsets": "更新偏移校准数据", "updated": "已更新", "usb_connected_no_port_info": "USB端口已连接", + "usb_drive_notification": "在运行开始前,请保持USB处于连接状态", "usb_port_connected": "USB端口{{port}}", "usb_port_number": "USB-{{port}}", "value_out_of_range_generic": "值必须在范围内", "value_out_of_range": "值必须在{{min}}-{{max}}之间", "value": "值", "values_are_view_only": "值仅供查看", + "variable_well_amount": "可变孔数", "view_current_offsets": "查看当前偏移量", "view_moam": "查看工作站中放置同类型模块的设置说明。", "view_setup_instructions": "查看设置说明", "volume": "体积", "what_labware_offset_is": "耗材偏移校准是一种位置调整类型,用于补偿甲板上耗材整体位置的微小实际差异。耗材偏移校准数据是耗材、甲板位和工作站的特定组合。", - "with_the_chosen_value": "使用选定的值时,发生以下错误:" + "with_the_chosen_value": "使用选定的值时,发生以下错误:", + "you_havent_confirmed": "您尚未确认 {{missingSteps}}。在继续运行协议之前,请确保这些步骤正确无误。" } diff --git a/app/src/assets/localization/zh/quick_transfer.json b/app/src/assets/localization/zh/quick_transfer.json index 651ace8ef4b..4a1e2779d52 100644 --- a/app/src/assets/localization/zh/quick_transfer.json +++ b/app/src/assets/localization/zh/quick_transfer.json @@ -1,46 +1,114 @@ { + "a_way_to_move_liquid": "一种将单一液体从一个实验耗材移动到另一个实验耗材的方法。", "add_or_remove_columns": "添加或移除列", "add_or_remove": "添加或移除", + "advanced_setting_disabled": "此移液的高级设置已禁用", "advanced_settings": "高级设置", + "air_gap_before_aspirating": "在吸液前设置空气间隙", + "air_gap_before_dispensing": "在分液前设置空气间隙", + "air_gap_capacity_error": "移液器空间已满,无法添加空气间隙。", + "air_gap_value": "{{volume}} µL", + "air_gap_volume_µL": "空气间隙体积(µL)", + "air_gap": "空气间隙", "all": "所有实验耗材", "always": "每次吸液前", + "aspirate_flow_rate_µL": "吸取流速(µL/s)", + "aspirate_flow_rate": "吸取流速", + "aspirate_settings": "吸取设置", + "aspirate_tip_position": "吸取移液器位置", "aspirate_volume": "每孔吸液体积", "aspirate_volume_µL": "每孔吸液体积(µL)", + "attach_pipette": "连接移液器", + "blow_out_after_dispensing": "分液后吹出", + "blow_out_destination_well": "目标孔", + "blow_out_into_destination_well": "到目标孔", + "blow_out_into_source_well": "到吸液孔", + "blow_out_into_trash_bin": "到垃圾桶", + "blow_out_into_waste_chute": "到外置垃圾槽", + "blow_out_source_well": "源孔", + "blow_out_trash_bin": "垃圾桶", + "blow_out_waste_chute": "外置垃圾槽", + "blow_out": "吹出", "both_mounts": "左侧+右侧支架", "change_tip": "更换吸头", "character_limit_error": "字数超出限制", "column": "列", "columns": "列", + "consolidate_volume_error": "所选的目标孔太小,无法合并。请尝试从更少的孔中合并。", + "create_new_to_edit": "创建新的快速移液以进行编辑", "create_new_transfer": "创建新的快速移液命令", + "create_to_get_started": "创建新的快速移液以开始操作。", "create_transfer": "创建移液命令", + "delay_before_aspirating": "吸取前的延迟", + "delay_before_dispensing": "分液前的延迟", + "delay_duration_s": "延迟时长(秒)", + "delay_position_mm": "距孔底延迟时的位置(mm)", + "delay_value": "{{delay}}秒,距离孔底{{position}}mm", + "delay": "延迟", + "delete_this_transfer": "确定删除此这个快速移液?", + "delete_transfer": "删除快速移液", + "deleted_transfer": "已删除快速移液", "destination": "目标", "destination_labware": "目标实验耗材", - "dispense_volume": "每孔排液体积", + "disabled": "已禁用", + "dispense_flow_rate_µL": "分液流速(µL/s)", + "dispense_flow_rate": "分液流速", + "dispense_settings": "分液设置", + "dispense_tip_position": "分液吸头位置", "dispense_volume_µL": "每孔排液体积(µL)", + "dispense_volume": "每孔排液体积", + "disposal_volume_µL": "废液量(µL)", + "distance_bottom_of_well_mm": "距离孔底的高度(mm)", + "distribute_volume_error": "所选源孔太小,无法从中分液。请尝试向更少的孔中分液。", "enter_characters": "输入最多60个字符", + "error_analyzing": "在尝试分析{{transferName}}时发生错误。", "exit_quick_transfer": "退出快速移液?", + "failed_analysis": "分析失败", + "flow_rate_value": "{{flow_rate}} µL/s", + "got_it": "明白了", "grid": "网格", "grids": "网格", + "labware": "实验耗材", "learn_more": "了解更多", "left_mount": "左侧支架", "lose_all_progress": "您将失去所有此快速移液流程进度.", + "mix_before_aspirating": "在吸液前混匀", + "mix_before_dispensing": "在分液前混匀", + "mix_repetitions": "混匀重复次数", + "mix_value": "{{volume}} µL,混匀{{reps}}次", + "mix_volume_µL": "混匀体积(µL)", + "mix": "混匀", "name_your_transfer": "为您的快速移液流程命名", + "none_to_show": "没有快速移液可显示!", "number_wells_selected_error_learn_more": "具有多个源孔{{selectionUnits}}的快速移液是可以进行一对一或者多对多移液的(为此移液流程同样选择{{wellCount}}个目标孔位{{selectionUnits}})或进行多对一移液,即合并为单个孔位(选择1个目标孔{{selectionUnit}})。", "number_wells_selected_error_message": "选择1个或{{wellCount}}个{{selectionUnits}}来进行此移液操作.", "once": "在移液开始时", + "option_disabled": "已禁用", + "option_enabled": "已启用", "overview": "概览", "perDest": "每个目标孔位", "perSource": "每个源孔位", + "pin_transfer": "快速移液", + "pinned_transfer": "固定快速移液", + "pinned_transfers": "固定快速移液", + "pipette_path_multi_aspirate": "多次吸取", + "pipette_path_multi_dispense_volume_blowout": "多次分液,{{volume}} 微升废弃量,在{{blowOutLocation}}吹出", + "pipette_path_multi_dispense": "多次分液", + "pipette_path_single": "单次转移", + "pipette_path": "移液器路径", + "pipette_currently_attached": "快速移液移液器选项取决于当前您工作站上安装的移液器.", "pipette": "移液器", + "pre_wet_tip": "润湿吸头", "quick_transfer_volume": "快速移液{{volume}}µL", + "quick_transfer": "快速移液", "right_mount": "右侧支架", "reservoir": "储液槽", "run_now": "立即运行", "run_quick_transfer_now": "您想立即运行快速移液流程吗?", + "run_transfer": "运行快速移液", "save": "保存", "save_to_run_later": "保存您的快速移液流程以备后续运行.", "save_for_later": "保存备用", - "source": "源", "select_attached_pipette": "选择已连接的移液器", "select_by": "按...选择", "select_dest_labware": "选择目标实验耗材", @@ -52,25 +120,40 @@ "set_dispense_volume": "设置排液体积", "set_transfer_volume": "设置移液体积", "source_labware": "源实验耗材", - "source_labware_d2": "D2位置的源实验耗材", + "source_labware_c2": "C2 中的源实验耗材", + "source": "源", "starting_well": "起始孔", + "storage_limit_reached": "已达到存储限制", "use_deck_slots": "快速移液将使用板位B2-D2。这些板位将用于放置吸头盒、源实验耗材和目标实验耗材。请确保使用最新的甲板配置,避免碰撞。", "tip_drop_location": "吸头丢弃位置", "tip_management": "吸头管理", + "tip_position_value": "距底部 {{position}} mm", + "tip_position": "移液器位置", "tip_rack": "吸头盒", + "too_many_pins_body": "删除一个快速移液,以便向您的固定列表中添加更多传输。", + "too_many_pins_header": "您已达到上限!", + "touch_tip_before_aspirating": "在吸液前做碰壁动作", + "touch_tip_before_dispensing": "在分液前做碰壁动作", + "touch_tip_position_mm": "在孔底部做碰壁动作的高度(mm)", + "touch_tip_value": "距底部 {{position}} mm", + "touch_tip": "碰壁动作", + "transfer_analysis_failed": "快速移液分析失败", + "transfer_name": "移液名称", "trashBin": "垃圾桶", "trashBin_location": "位于{{slotName}}的垃圾桶", "tubeRack": "试管架", + "unpin_transfer": "取消固定的快速移液", + "unpinned_transfer": "已取消固定的快速移液", "volume_per_well": "每孔体积", "volume_per_well_µL": "每孔体积(µL)", "value_out_of_range": "值必须在{{min}}-{{max}}之间", - "labware": "实验耗材", - "pipette_currently_attached": "快速移液移液器选项取决于当前您工作站上安装的移液器.", "wasteChute": "外置垃圾槽", "wasteChute_location": "位于{{slotName}}的外置垃圾槽", + "welcome_to_quick_transfer": "欢迎使用快速移液!", "wellPlate": "孔板", "well_selection": "孔位选择", "well_ratio": "快速移液可以一对一或者多对多进行移液的(为此移液操作选择同样数量的{{wells}})或可以多对一,也就是合并为单孔(选择1个目标孔位)。", "well": "孔", - "wells": "孔" + "wells": "孔", + "will_be_deleted": "{{transferName}} 将被永久删除。" } diff --git a/app/src/assets/localization/zh/robot_calibration.json b/app/src/assets/localization/zh/robot_calibration.json index 8f83fb649f8..d5959a113c6 100644 --- a/app/src/assets/localization/zh/robot_calibration.json +++ b/app/src/assets/localization/zh/robot_calibration.json @@ -53,6 +53,8 @@ "download_calibration_data_unavailable": "无校准数据可用。", "download_calibration_title": "下载校准数据", "download_details": "下载详情JSON校准文件查看摘要", + "error": "错误", + "exit": "退出", "finish": "完成", "get_started": "开始", "good_calibration": "良好校准", diff --git a/app/src/assets/localization/zh/run_details.json b/app/src/assets/localization/zh/run_details.json index 648df8a66e2..00d584bb4ba 100644 --- a/app/src/assets/localization/zh/run_details.json +++ b/app/src/assets/localization/zh/run_details.json @@ -17,6 +17,7 @@ "canceling_run": "正在取消运行", "clear_protocol_to_make_available": "清除工作站的协议以使其可用", "clear_protocol": "清除协议", + "close_door_to_resume_run": "关闭工作站门以继续运行", "close_door_to_resume": "关闭移液工作站的前门以继续运行", "close_door": "关闭移液工作站前门", "closing_protocol": "正在关闭协议", @@ -32,6 +33,7 @@ "date": "日期", "door_is_open": "工作站前门已打开", "door_open_pause": "当前步骤 - 暂停 - 前门已打开", + "download_files": "下载文件", "download": "下载", "download_run_log": "下载运行日志", "downloading_run_log": "正在下载运行日志", @@ -40,6 +42,7 @@ "end_of_protocol": "协议结束", "end_step_time": "结束", "end": "结束", + "error_details": "错误详情", "error_info": "错误{{errorCode}}:{{errorType}}", "error_type": "错误:{{errorType}}", "failed_step": "步骤失败", @@ -49,7 +52,6 @@ "labware": "耗材", "left": "左", "listed_values": "列出的值仅供查看", - "load_labware_info_protocol_setup_adapter_module": "在{{slot_name}}号板位中的{{module_name}}内加载{{labware}}的{{adapter_name}}", "load_labware_info_protocol_setup_adapter_off_deck": "在甲板外的{{adapter_name}}上加载{{labware}}", "load_labware_info_protocol_setup_adapter": "在{{slot_name}}号板位中的{{adapter_name}}中加载{{labware}}", "load_labware_info_protocol_setup_no_module": "在{{slot_name}}号板位中加载{{labware}}", @@ -68,6 +70,10 @@ "move_labware": "移动耗材", "name": "名称", "no_files_included": "未包含协议文件", + "no_of_error": "{{count}}个错误", + "no_of_errors": "{{count}}个错误", + "no_of_warning": "{{count}}个警告", + "no_of_warnings": "{{count}}个警告", "no_offsets_available": "无耗材校准数据可用", "not_available_for_a_completed_run": "不适用于已完成的运行", "not_available_for_a_run_in_progress": "不适用于正在进行的运行", @@ -96,15 +102,21 @@ "protocol_title": "协议 -{{protocol_name}}", "resume_run": "恢复运行", "return_to_dashboard": "返回控制面板", + "return_to_quick_transfer": "返回快速移液", "right": "右", "robot_has_previous_offsets": "该移液工作站已存储了之前运行协议的耗材校准数据。您想将这些数据应用于此协议的运行吗?您仍然可以通过实验器具位置检查调整校准数据。", "robot_was_recalibrated": "在储存此耗材校准数据后,移液工作站已重新校准", "run_again": "再次运行", "run_canceled_splash": "运行已取消", + "run_canceled_with_errors_splash": "因错误取消运行。", + "run_canceled_with_errors": "因错误取消运行。", "run_canceled": "运行已取消。", + "run_completed_splash": "运行完成", + "run_completed_with_warnings_splash": "运行完成,并伴有警告。", + "run_completed_with_warnings": "运行完成,并伴有警告。", + "run_completed": "运行已完成。", "run_complete_splash": "运行已完成", "run_complete": "运行已完成", - "run_completed": "运行已完成。", "run_cta_disabled": "在开始运行之前,请完成协议选项卡上的所有必要步骤。", "run_failed_modal_body": "在协议执行{{command}}时发生错误。", "run_failed_modal_header": "{{errorName}}:{{errorCode}}协议步骤{{count}}", @@ -124,6 +136,8 @@ "start_step_time": "开始", "start_time": "开始时间", "start": "开始", + "status_awaiting-recovery-blocked-by-open-door": "暂停 - 门已打开", + "status_awaiting-recovery-paused": "暂停", "status_awaiting-recovery": "等待恢复", "status_blocked-by-open-door": "暂停 - 前门打开", "status_failed": "失败", @@ -150,5 +164,7 @@ "view_analysis_error_details": "查看 错误详情", "view_current_step": "查看当前步骤", "view_error_details": "查看错误详情", - "view_error": "查看错误" + "view_error": "查看错误", + "view_warning_details": "查看警告详情", + "warning_details": "警告详情" } diff --git a/app/src/assets/localization/zh/shared.json b/app/src/assets/localization/zh/shared.json index f9b0aa47cca..90b597b2820 100644 --- a/app/src/assets/localization/zh/shared.json +++ b/app/src/assets/localization/zh/shared.json @@ -2,6 +2,7 @@ "a_software_update_is_available": "此工作站有可用的软件更新。更新以运行协议。", "add": "添加", "alphabetical": "按字母排序", + "another_app_controlling_robot": "工作站的触摸屏或另一台装有应用程序的电脑正在控制这个工作站。", "back": "返回", "before_you_begin": "在您开始之前", "browse": "浏览", @@ -26,7 +27,7 @@ "disabled_protocol_is_running": "协议正在运行", "dont_show_me_again": "不再显示", "drag_and_drop": "拖放或 浏览 您的文件", - "empty": "清空", + "empty": "空闲", "ending": "结束中", "error_encountered": "遇到错误", "error": "错误", diff --git a/app/src/assets/localization/zh/top_navigation.json b/app/src/assets/localization/zh/top_navigation.json index c3906955266..cb831731be9 100644 --- a/app/src/assets/localization/zh/top_navigation.json +++ b/app/src/assets/localization/zh/top_navigation.json @@ -13,6 +13,7 @@ "please_load_a_protocol": "请加载协议以继续", "protocol_runs": "协议运行", "protocols": "协议", + "quick_transfer": "快速移液", "robot_settings": "工作站设置", "run": "运行", "settings": "设置" diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 0a9701a8e87..e1c772cb582 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -7,6 +7,20 @@ import { titleCase } from '@opentrons/shared-data' import type { InitOptions } from 'i18next' +export const US_ENGLISH = 'en-US' +export const SIMPLIFIED_CHINESE = 'zh-CN' + +// these strings will not be translated so should not be localized +export const US_ENGLISH_DISPLAY_NAME = 'English (US)' +export const SIMPLIFIED_CHINESE_DISPLAY_NAME = '中文' + +export type Language = typeof US_ENGLISH | typeof SIMPLIFIED_CHINESE + +export const LANGUAGES: Array<{ name: string; value: Language }> = [ + { name: US_ENGLISH_DISPLAY_NAME, value: US_ENGLISH }, + { name: SIMPLIFIED_CHINESE_DISPLAY_NAME, value: SIMPLIFIED_CHINESE }, +] + const i18nConfig: InitOptions = { resources, lng: 'en', diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/index.tsx b/app/src/local-resources/commands/hooks/useCommandTextString/index.ts similarity index 98% rename from app/src/local-resources/commands/hooks/useCommandTextString/index.tsx rename to app/src/local-resources/commands/hooks/useCommandTextString/index.ts index 3966a1bc7f4..0eb04ee588e 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/index.tsx +++ b/app/src/local-resources/commands/hooks/useCommandTextString/index.ts @@ -7,11 +7,11 @@ import type { RobotType, LabwareDefinition2, } from '@opentrons/shared-data' -import type { GetDirectTranslationCommandText } from './utils/getDirectTranslationCommandText' import type { TCProfileStepText, TCProfileCycleText, -} from './utils/getTCRunExtendedProfileCommandText' + GetDirectTranslationCommandText, +} from './utils' import type { CommandTextData } from '/app/local-resources/commands/types' export interface UseCommandTextStringParams { diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/__tests__/getPipettingCommandText.test.tsx b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/__tests__/getPipettingCommandText.test.tsx new file mode 100644 index 00000000000..82b269bd581 --- /dev/null +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/__tests__/getPipettingCommandText.test.tsx @@ -0,0 +1,186 @@ +import { screen } from '@testing-library/react' +import { vi, describe, it, beforeEach } from 'vitest' +import { useTranslation } from 'react-i18next' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { + getLabwareDefinitionsFromCommands, + getLabwareName, + getLoadedLabware, + getLabwareDisplayLocation, +} from '/app/local-resources/labware' +import { getPipettingCommandText } from '../getPipettingCommandText' +import { getLabwareDefURI } from '@opentrons/shared-data' +import { getFinalLabwareLocation } from '../../getFinalLabwareLocation' +import { getWellRange } from '../../getWellRange' +import { getFinalMoveToAddressableAreaCmd } from '../../getFinalAddressableAreaCmd' +import { getAddressableAreaDisplayName } from '../../getAddressableAreaDisplayName' + +vi.mock('@opentrons/shared-data') +vi.mock('../../getFinalLabwareLocation') +vi.mock('../../getWellRange') +vi.mock('/app/local-resources/labware') +vi.mock('../../getFinalAddressableAreaCmd') +vi.mock('../../getAddressableAreaDisplayName') + +const baseCommandData = { + allRunDefs: {}, + robotType: 'OT-2', + commandTextData: { + commands: [], + labware: [], + modules: [], + pipettes: [{ id: 'pipette-1', pipetteName: 'p300_single' }], + }, +} as any + +function TestWrapper({ command }: { command: any }): JSX.Element { + const { t } = useTranslation('protocol_command_text') + const text = getPipettingCommandText({ + command, + ...baseCommandData, + t, + }) + + return
{text}
+} + +const render = (command: any) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('getPipettingCommandText', () => { + beforeEach(() => { + vi.mocked(getLabwareDefURI).mockImplementation((def: any) => def.uri) + vi.mocked(getFinalLabwareLocation).mockReturnValue('slot-1' as any) + vi.mocked(getWellRange).mockReturnValue('A1') + vi.mocked(getLabwareDefinitionsFromCommands).mockReturnValue([ + { uri: 'tiprack-uri', parameters: { isTiprack: true } }, + { uri: 'plate-uri', parameters: { isTiprack: false } }, + ] as any) + vi.mocked(getLabwareName).mockReturnValue('Test Labware') + vi.mocked(getLoadedLabware).mockImplementation( + (labware, id) => + ({ + definitionUri: id === 'tiprack-id' ? 'tiprack-uri' : 'plate-uri', + } as any) + ) + vi.mocked(getLabwareDisplayLocation).mockReturnValue('Slot 1') + vi.mocked(getFinalMoveToAddressableAreaCmd).mockReturnValue({ + id: 'cmd-1', + commandType: 'moveToAddressableArea', + } as any) + vi.mocked(getAddressableAreaDisplayName).mockReturnValue('Fixed Trash') + }) + + it('should render aspirate command text correctly', () => { + const command = { + id: 'cmd-1', + commandType: 'aspirate', + params: { + labwareId: 'labware-1', + wellName: 'A1', + volume: 100, + flowRate: 150, + }, + } + + render(command) + screen.getByText( + /Aspirating 100 µL from well A1 of Test Labware in Slot 1 at 150 µL\/sec/ + ) + }) + + it('should render dispense command text correctly', () => { + const command = { + id: 'cmd-1', + commandType: 'dispense', + params: { + labwareId: 'labware-1', + wellName: 'A1', + volume: 100, + flowRate: 150, + }, + } + + render(command) + screen.getByText( + /Dispensing 100 µL into well A1 of Test Labware in Slot 1 at 150 µL\/sec/ + ) + }) + + it('should render dispense with push out command text correctly', () => { + const command = { + id: 'cmd-1', + commandType: 'dispense', + params: { + labwareId: 'labware-1', + wellName: 'A1', + volume: 100, + flowRate: 150, + pushOut: 10, + }, + } + + render(command) + screen.getByText( + /Dispensing 100 µL into well A1 of Test Labware in Slot 1 at 150 µL\/sec and pushing out 10 µL/ + ) + }) + + it('should render pickup tip command text correctly', () => { + const command = { + id: 'cmd-1', + commandType: 'pickUpTip', + params: { + labwareId: 'tiprack-id', + wellName: 'A1', + pipetteId: 'pipette-1', + }, + } + + render(command) + screen.getByText(/Picking up tip\(s\) from A1 of Test Labware in Slot 1/) + }) + + it('should render drop tip in tiprack command text correctly', () => { + const command = { + id: 'cmd-1', + commandType: 'dropTip', + params: { + labwareId: 'tiprack-id', + wellName: 'A1', + }, + } + + render(command) + screen.getByText(/Returning tip to A1 of Test Labware in Slot 1/) + }) + + it('should render drop tip in place command text correctly if there is an addressable area name', () => { + const command = { + id: 'cmd-1', + commandType: 'dropTipInPlace', + params: {}, + } + + render(command) + screen.getByText('Dropping tip in Fixed Trash') + }) + + it('should render drop tip in place command text correctly if there is not an addressable area name', () => { + const command = { + id: 'cmd-1', + commandType: 'dropTipInPlace', + params: {}, + } + + vi.mocked(getFinalMoveToAddressableAreaCmd).mockReturnValue(null) + + render(command) + screen.getByText('Dropping tip in place') + }) +}) diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getAbsorbanceReaderCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getAbsorbanceReaderCommandText.ts similarity index 97% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getAbsorbanceReaderCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getAbsorbanceReaderCommandText.ts index b9e7107b569..926ed749609 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getAbsorbanceReaderCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getAbsorbanceReaderCommandText.ts @@ -5,7 +5,7 @@ import type { AbsorbanceReaderReadRunTimeCommand, RunTimeCommand, } from '@opentrons/shared-data' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export type AbsorbanceCreateCommand = | AbsorbanceReaderOpenLidRunTimeCommand diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getCommentCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getCommentCommandText.ts similarity index 83% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getCommentCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getCommentCommandText.ts index 3a1b7ce7e8a..f50de82c96e 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getCommentCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getCommentCommandText.ts @@ -1,5 +1,5 @@ import type { CommentRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getCommentCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getConfigureForVolumeCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureForVolumeCommandText.ts similarity index 92% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getConfigureForVolumeCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureForVolumeCommandText.ts index 1a4ee2e7c0e..fd9456b4cc3 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getConfigureForVolumeCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureForVolumeCommandText.ts @@ -1,7 +1,7 @@ import { getPipetteSpecsV2 } from '@opentrons/shared-data' import type { ConfigureForVolumeRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getConfigureForVolumeCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getConfigureNozzleLayoutCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts similarity index 95% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getConfigureNozzleLayoutCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts index 04d476fadd1..8c9e12f3d5b 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getConfigureNozzleLayoutCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts @@ -1,7 +1,7 @@ import { getPipetteSpecsV2 } from '@opentrons/shared-data' import type { ConfigureNozzleLayoutRunTimeCommand } from '@opentrons/shared-data' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getConfigureNozzleLayoutCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getCustomCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getCustomCommandText.ts similarity index 91% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getCustomCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getCustomCommandText.ts index da6d5a1d506..97d60249f7e 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getCustomCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getCustomCommandText.ts @@ -1,5 +1,5 @@ import type { CustomRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getCustomCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getDelayCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getDelayCommandText.ts similarity index 91% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getDelayCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getDelayCommandText.ts index 8bb24d99661..f421a163b36 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getDelayCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getDelayCommandText.ts @@ -1,5 +1,5 @@ import type { DeprecatedDelayRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getDelayCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getDirectTranslationCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getDirectTranslationCommandText.ts similarity index 97% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getDirectTranslationCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getDirectTranslationCommandText.ts index fd586136e90..92e969f402b 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getDirectTranslationCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getDirectTranslationCommandText.ts @@ -1,5 +1,5 @@ import type { RunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' const SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE: { [commandType in RunTimeCommand['commandType']]?: string diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getHSShakeSpeedCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getHSShakeSpeedCommandText.ts similarity index 87% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getHSShakeSpeedCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getHSShakeSpeedCommandText.ts index 3710e7f0930..157cde89212 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getHSShakeSpeedCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getHSShakeSpeedCommandText.ts @@ -1,5 +1,5 @@ import type { HeaterShakerSetAndWaitForShakeSpeedRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getHSShakeSpeedCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getLiquidProbeCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getLiquidProbeCommandText.ts similarity index 92% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getLiquidProbeCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getLiquidProbeCommandText.ts index 171667012fe..014d30318eb 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getLiquidProbeCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getLiquidProbeCommandText.ts @@ -2,14 +2,14 @@ import { getLabwareName, getLabwareDisplayLocation, } from '/app/local-resources/labware' -import { getFinalLabwareLocation } from './getFinalLabwareLocation' +import { getFinalLabwareLocation } from '../getFinalLabwareLocation' import type { LiquidProbeRunTimeCommand, RunTimeCommand, TryLiquidProbeRunTimeCommand, } from '@opentrons/shared-data' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' type LiquidProbeRunTimeCommands = | LiquidProbeRunTimeCommand diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getLoadCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getLoadCommandText.ts similarity index 97% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getLoadCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getLoadCommandText.ts index d8ab8736e08..cba135218c8 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getLoadCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getLoadCommandText.ts @@ -5,8 +5,8 @@ import { getPipetteSpecsV2, } from '@opentrons/shared-data' -import { getPipetteNameOnMount } from './getPipetteNameOnMount' -import { getLiquidDisplayName } from './getLiquidDisplayName' +import { getPipetteNameOnMount } from '../getPipetteNameOnMount' +import { getLiquidDisplayName } from '../getLiquidDisplayName' import { getLabwareName } from '/app/local-resources/labware' import { @@ -15,7 +15,7 @@ import { } from '/app/local-resources/modules' import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' -import type { GetCommandText } from '..' +import type { GetCommandText } from '../..' export const getLoadCommandText = ({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveLabwareCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveLabwareCommandText.ts similarity index 94% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveLabwareCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveLabwareCommandText.ts index 67fe3d52aaf..29e90946bb4 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveLabwareCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveLabwareCommandText.ts @@ -1,13 +1,13 @@ import { GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA } from '@opentrons/shared-data' -import { getFinalLabwareLocation } from './getFinalLabwareLocation' +import { getFinalLabwareLocation } from '../getFinalLabwareLocation' import { getLabwareName, getLabwareDisplayLocation, } from '/app/local-resources/labware' import type { MoveLabwareRunTimeCommand } from '@opentrons/shared-data' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveLabwareCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveRelativeCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveRelativeCommandText.ts similarity index 86% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveRelativeCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveRelativeCommandText.ts index 7f3f8bf0aaa..d104e522fcd 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveRelativeCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveRelativeCommandText.ts @@ -1,5 +1,5 @@ import type { MoveRelativeRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveRelativeCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToAddressableAreaCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToAddressableAreaCommandText.ts similarity index 67% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToAddressableAreaCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToAddressableAreaCommandText.ts index 749ef30f451..5dd4adb4ca4 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToAddressableAreaCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToAddressableAreaCommandText.ts @@ -1,7 +1,7 @@ -import { getAddressableAreaDisplayName } from './getAddressableAreaDisplayName' +import { getAddressableAreaDisplayName } from '../getAddressableAreaDisplayName' import type { MoveToAddressableAreaRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveToAddressableAreaCommandText({ command, @@ -10,7 +10,7 @@ export function getMoveToAddressableAreaCommandText({ }: HandlesCommands): string { const addressableAreaDisplayName = commandTextData != null - ? getAddressableAreaDisplayName(commandTextData, command.id, t) + ? getAddressableAreaDisplayName(commandTextData.commands, command.id, t) : null return t('move_to_addressable_area', { diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToAddressabelAreaForDropTipCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToAddressableAreaForDropTipCommandText.ts similarity index 69% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToAddressabelAreaForDropTipCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToAddressableAreaForDropTipCommandText.ts index f7cc0f42e1f..29cd446a9ad 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToAddressabelAreaForDropTipCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToAddressableAreaForDropTipCommandText.ts @@ -1,7 +1,7 @@ -import { getAddressableAreaDisplayName } from './getAddressableAreaDisplayName' +import { getAddressableAreaDisplayName } from '../getAddressableAreaDisplayName' import type { MoveToAddressableAreaForDropTipRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveToAddressableAreaForDropTipCommandText({ command, @@ -10,7 +10,7 @@ export function getMoveToAddressableAreaForDropTipCommandText({ }: HandlesCommands): string { const addressableAreaDisplayName = commandTextData != null - ? getAddressableAreaDisplayName(commandTextData, command.id, t) + ? getAddressableAreaDisplayName(commandTextData.commands, command.id, t) : null return t('move_to_addressable_area_drop_tip', { diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToCoordinatesCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToCoordinatesCommandText.ts similarity index 86% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToCoordinatesCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToCoordinatesCommandText.ts index a3dc5ace9fe..fde6e5aff22 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToCoordinatesCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToCoordinatesCommandText.ts @@ -1,5 +1,5 @@ import type { MoveToCoordinatesRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveToCoordinatesCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToSlotCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToSlotCommandText.ts similarity index 85% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToSlotCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToSlotCommandText.ts index b66f5d78513..75904b7cb43 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToSlotCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToSlotCommandText.ts @@ -1,5 +1,5 @@ import type { MoveToSlotRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveToSlotCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToWellCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToWellCommandText.ts similarity index 91% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToWellCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToWellCommandText.ts index e3c8d6223be..50bdba0a52f 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getMoveToWellCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getMoveToWellCommandText.ts @@ -3,10 +3,10 @@ import { getLabwareDisplayLocation, } from '/app/local-resources/labware' -import { getFinalLabwareLocation } from './getFinalLabwareLocation' +import { getFinalLabwareLocation } from '../getFinalLabwareLocation' import type { MoveToWellRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getMoveToWellCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getPipettingCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts similarity index 88% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getPipettingCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts index 34ad5eae3a3..6ef1369691e 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getPipettingCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts @@ -1,7 +1,7 @@ import { getLabwareDefURI } from '@opentrons/shared-data' -import { getFinalLabwareLocation } from './getFinalLabwareLocation' -import { getWellRange } from './getWellRange' +import { getFinalLabwareLocation } from '../getFinalLabwareLocation' +import { getWellRange } from '../getWellRange' import { getLabwareDefinitionsFromCommands, @@ -11,7 +11,9 @@ import { } from '/app/local-resources/labware' import type { PipetteName, RunTimeCommand } from '@opentrons/shared-data' -import type { GetCommandText } from '..' +import type { GetCommandText } from '../..' +import { getFinalMoveToAddressableAreaCmd } from '/app/local-resources/commands/hooks/useCommandTextString/utils/getFinalAddressableAreaCmd' +import { getAddressableAreaDisplayName } from '/app/local-resources/commands/hooks/useCommandTextString/utils/getAddressableAreaDisplayName' export const getPipettingCommandText = ({ command, @@ -186,7 +188,14 @@ export const getPipettingCommandText = ({ }) } case 'dropTipInPlace': { - return t('drop_tip_in_place') + const cmd = getFinalMoveToAddressableAreaCmd(allPreviousCommands ?? []) + + if (cmd != null) { + const displayName = getAddressableAreaDisplayName([cmd], cmd?.id, t) + return t('dropping_tip_in_trash', { trash: displayName }) + } else { + return t('drop_tip_in_place') + } } case 'dispenseInPlace': { const { volume, flowRate } = command.params diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getPrepareToAspirateCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPrepareToAspirateCommandText.ts similarity index 92% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getPrepareToAspirateCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPrepareToAspirateCommandText.ts index 13d32b6b7d6..f0d68c3fd4d 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getPrepareToAspirateCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPrepareToAspirateCommandText.ts @@ -1,7 +1,7 @@ import { getPipetteSpecsV2 } from '@opentrons/shared-data' import type { PrepareToAspirateRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getPrepareToAspirateCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getRailLightsCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getRailLightsCommandText.ts similarity index 89% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getRailLightsCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getRailLightsCommandText.ts index b731d3ec392..8fe4c18aa4b 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getRailLightsCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getRailLightsCommandText.ts @@ -1,5 +1,5 @@ import type { RunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' type HandledCommands = Extract diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTCRunExtendedProfileCommandText.ts similarity index 97% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTCRunExtendedProfileCommandText.ts index 4c4acde0b6f..2d09f07f28c 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTCRunExtendedProfileCommandText.ts @@ -4,8 +4,8 @@ import type { TCProfileCycle, AtomicProfileStep, } from '@opentrons/shared-data/command' -import type { GetTCRunExtendedProfileCommandTextResult } from '..' -import type { HandlesCommands } from './types' +import type { GetTCRunExtendedProfileCommandTextResult } from '../..' +import type { HandlesCommands } from '../types' export interface TCProfileStepText { kind: 'step' diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTCRunProfileCommandText.ts similarity index 87% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTCRunProfileCommandText.ts index cbc56b02635..a98ce9cfa4a 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTCRunProfileCommandText.ts @@ -1,7 +1,7 @@ import { formatDurationLabeled } from '/app/transformations/commands' import type { TCRunProfileRunTimeCommand } from '@opentrons/shared-data/command' -import type { GetTCRunProfileCommandTextResult } from '..' -import type { HandlesCommands } from './types' +import type { GetTCRunProfileCommandTextResult } from '../..' +import type { HandlesCommands } from '../types' export function getTCRunProfileCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getTemperatureCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTemperatureCommandText.ts similarity index 97% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getTemperatureCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTemperatureCommandText.ts index ee60a76c289..1b5a03745c3 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getTemperatureCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getTemperatureCommandText.ts @@ -6,7 +6,7 @@ import type { HeaterShakerSetTargetTemperatureCreateCommand, RunTimeCommand, } from '@opentrons/shared-data' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export type TemperatureCreateCommand = | TemperatureModuleSetTargetTemperatureCreateCommand diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getUnknownCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getUnknownCommandText.ts similarity index 71% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getUnknownCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getUnknownCommandText.ts index 4f2346c7c01..17b69b84c6a 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getUnknownCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getUnknownCommandText.ts @@ -1,4 +1,4 @@ -import type { GetCommandText } from '..' +import type { GetCommandText } from '../..' export function getUnknownCommandText({ command }: GetCommandText): string { return JSON.stringify(command) diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getWaitForDurationCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getWaitForDurationCommandText.ts similarity index 87% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getWaitForDurationCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getWaitForDurationCommandText.ts index d3b3136be1f..18ccc55540a 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getWaitForDurationCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getWaitForDurationCommandText.ts @@ -1,5 +1,5 @@ import type { WaitForDurationRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getWaitForDurationCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getWaitForResumeCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getWaitForResumeCommandText.ts similarity index 87% rename from app/src/local-resources/commands/hooks/useCommandTextString/utils/getWaitForResumeCommandText.ts rename to app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getWaitForResumeCommandText.ts index f1c7b7fcef6..a591504b244 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getWaitForResumeCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getWaitForResumeCommandText.ts @@ -1,5 +1,5 @@ import type { WaitForResumeRunTimeCommand } from '@opentrons/shared-data/command' -import type { HandlesCommands } from './types' +import type { HandlesCommands } from '../types' export function getWaitForResumeCommandText({ command, diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/index.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/index.ts new file mode 100644 index 00000000000..c2926c880c6 --- /dev/null +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/index.ts @@ -0,0 +1,26 @@ +export * from './getLoadCommandText' +export * from './getTemperatureCommandText' +export * from './getTCRunProfileCommandText' +export * from './getTCRunExtendedProfileCommandText' +export * from './getHSShakeSpeedCommandText' +export * from './getMoveToSlotCommandText' +export * from './getMoveRelativeCommandText' +export * from './getMoveToCoordinatesCommandText' +export * from './getMoveToWellCommandText' +export * from './getMoveLabwareCommandText' +export * from './getConfigureForVolumeCommandText' +export * from './getConfigureNozzleLayoutCommandText' +export * from './getPrepareToAspirateCommandText' +export * from './getMoveToAddressableAreaCommandText' +export * from './getMoveToAddressableAreaForDropTipCommandText' +export * from './getDirectTranslationCommandText' +export * from './getWaitForDurationCommandText' +export * from './getWaitForResumeCommandText' +export * from './getDelayCommandText' +export * from './getCommentCommandText' +export * from './getCustomCommandText' +export * from './getUnknownCommandText' +export * from './getPipettingCommandText' +export * from './getLiquidProbeCommandText' +export * from './getRailLightsCommandText' +export * from './getAbsorbanceReaderCommandText' diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getAddressableAreaDisplayName.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/getAddressableAreaDisplayName.ts index 20d7c6cca07..c3160de6223 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getAddressableAreaDisplayName.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/getAddressableAreaDisplayName.ts @@ -1,17 +1,16 @@ import type { AddressableAreaName, MoveToAddressableAreaParams, + RunTimeCommand, } from '@opentrons/shared-data' import type { TFunction } from 'i18next' -import type { CommandTextData } from '/app/local-resources/commands' - export function getAddressableAreaDisplayName( - commandTextData: CommandTextData, + commands: RunTimeCommand[] | undefined, commandId: string, t: TFunction ): string { - const addressableAreaCommand = (commandTextData?.commands ?? []).find( + const addressableAreaCommand = (commands ?? []).find( command => command.id === commandId ) @@ -30,8 +29,11 @@ export function getAddressableAreaDisplayName( return t('trash_bin_in_slot', { slot_name: slotName }) } else if (addressableAreaName.includes('WasteChute')) { return t('waste_chute') - } else if (addressableAreaName === 'fixedTrash') return t('fixed_trash') - else return addressableAreaName + } else if (addressableAreaName === 'fixedTrash') { + return t('fixed_trash') + } else { + return addressableAreaName + } } const getMovableTrashSlot = ( diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalAddressableAreaCmd.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalAddressableAreaCmd.ts new file mode 100644 index 00000000000..3471073a8b9 --- /dev/null +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalAddressableAreaCmd.ts @@ -0,0 +1,21 @@ +import { findLastAt } from '/app/local-resources/commands/hooks/useCommandTextString/utils/helpers' + +import type { RunTimeCommand } from '@opentrons/shared-data' +/** + * given a list of commands and a labwareId, calculate the resulting location + * of the corresponding labware after all given commands are executed + * @param commands list of commands to search within + * @returns The last command related to addressable areas. + */ +export function getFinalMoveToAddressableAreaCmd( + commands: RunTimeCommand[] +): RunTimeCommand | null { + const [cmd] = findLastAt( + commands, + (c: RunTimeCommand) => + c.commandType === 'moveToAddressableArea' || + c.commandType === 'moveToAddressableAreaForDropTip' + ) + + return cmd ?? null +} diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalLabwareLocation.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalLabwareLocation.ts index e674794a265..7e73770cbbd 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalLabwareLocation.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/getFinalLabwareLocation.ts @@ -1,3 +1,5 @@ +import { findLastAt } from './helpers' + import type { LabwareLocation, RunTimeCommand, @@ -5,22 +7,6 @@ import type { MoveLabwareRunTimeCommand, } from '@opentrons/shared-data' -const findLastAt = ( - arr: readonly T[], - pred: ((el: T) => boolean) | ((el: T) => el is U) -): [U, number] | [undefined, -1] => { - let arrayLoc = -1 - const lastEl = arr.findLast((el: T, idx: number): el is U => { - arrayLoc = idx - return pred(el) - }) - if (lastEl === undefined) { - return [undefined, -1] - } else { - return [lastEl, arrayLoc] - } -} - /** * given a list of commands and a labwareId, calculate the resulting location * of the corresponding labware after all given commands are executed diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/helpers.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/helpers.ts new file mode 100644 index 00000000000..5d28c41b82c --- /dev/null +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/helpers.ts @@ -0,0 +1,15 @@ +export const findLastAt = ( + arr: readonly T[], + pred: ((el: T) => boolean) | ((el: T) => el is U) +): [U, number] | [undefined, -1] => { + let arrayLoc = -1 + const lastEl = arr.findLast((el: T, idx: number): el is U => { + arrayLoc = idx + return pred(el) + }) + if (lastEl === undefined) { + return [undefined, -1] + } else { + return [lastEl, arrayLoc] + } +} diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/index.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/index.ts index 76659ca1222..44f99055f08 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/index.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/index.ts @@ -1,26 +1,2 @@ -export { getLoadCommandText } from './getLoadCommandText' -export { getTemperatureCommandText } from './getTemperatureCommandText' -export { getTCRunProfileCommandText } from './getTCRunProfileCommandText' -export { getTCRunExtendedProfileCommandText } from './getTCRunExtendedProfileCommandText' -export { getHSShakeSpeedCommandText } from './getHSShakeSpeedCommandText' -export { getMoveToSlotCommandText } from './getMoveToSlotCommandText' -export { getMoveRelativeCommandText } from './getMoveRelativeCommandText' -export { getMoveToCoordinatesCommandText } from './getMoveToCoordinatesCommandText' -export { getMoveToWellCommandText } from './getMoveToWellCommandText' -export { getMoveLabwareCommandText } from './getMoveLabwareCommandText' -export { getConfigureForVolumeCommandText } from './getConfigureForVolumeCommandText' -export { getConfigureNozzleLayoutCommandText } from './getConfigureNozzleLayoutCommandText' -export { getPrepareToAspirateCommandText } from './getPrepareToAspirateCommandText' -export { getMoveToAddressableAreaCommandText } from './getMoveToAddressableAreaCommandText' -export { getMoveToAddressableAreaForDropTipCommandText } from './getMoveToAddressabelAreaForDropTipCommandText' -export { getDirectTranslationCommandText } from './getDirectTranslationCommandText' -export { getWaitForDurationCommandText } from './getWaitForDurationCommandText' -export { getWaitForResumeCommandText } from './getWaitForResumeCommandText' -export { getDelayCommandText } from './getDelayCommandText' -export { getCommentCommandText } from './getCommentCommandText' -export { getCustomCommandText } from './getCustomCommandText' -export { getUnknownCommandText } from './getUnknownCommandText' -export { getPipettingCommandText } from './getPipettingCommandText' -export { getLiquidProbeCommandText } from './getLiquidProbeCommandText' -export { getRailLightsCommandText } from './getRailLightsCommandText' -export { getAbsorbanceReaderCommandText } from './getAbsorbanceReaderCommandText' +export * from './commandText' +export * from './types' diff --git a/app/src/molecules/Command/__tests__/CommandText.test.tsx b/app/src/molecules/Command/__tests__/CommandText.test.tsx index 6999063be38..f2762b622d7 100644 --- a/app/src/molecules/Command/__tests__/CommandText.test.tsx +++ b/app/src/molecules/Command/__tests__/CommandText.test.tsx @@ -444,7 +444,7 @@ describe('CommandText', () => { />, { i18nInstance: i18n } ) - screen.getByText('Dropping tip in place') + screen.getByText('Dropping tip in D3') }) it('renders correct text for pickUpTip', () => { const command = mockCommandTextData.commands.find( diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx index 3a378d5f04f..a91d7389072 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx @@ -99,7 +99,7 @@ describe('SystemLanguagePreferenceModal', () => { it('should set a supported app language when system language is an unsupported locale of the same language', () => { vi.mocked(getAppLanguage).mockReturnValue(null) - vi.mocked(getSystemLanguage).mockReturnValue('en-UK') + vi.mocked(getSystemLanguage).mockReturnValue('en-GB') render() @@ -116,7 +116,7 @@ describe('SystemLanguagePreferenceModal', () => { 'language.appLanguage', MOCK_DEFAULT_LANGUAGE ) - expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-UK') + expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-GB') }) it('should render the correct header, description, and buttons when system language changes', () => { diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx index 42ff74a0eb8..59ae8640f92 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx @@ -15,6 +15,7 @@ import { StyledText, } from '@opentrons/components' +import { LANGUAGES } from '/app/i18n' import { getAppLanguage, getStoredSystemLanguage, @@ -26,18 +27,12 @@ import { getSystemLanguage } from '/app/redux/shell' import type { DropdownOption } from '@opentrons/components' import type { Dispatch } from '/app/redux/types' -// these strings will not be translated so should not be localized -const languageOptions: DropdownOption[] = [ - { name: 'English (US)', value: 'en-US' }, - { name: '中文', value: 'zh-CN' }, -] - export function SystemLanguagePreferenceModal(): JSX.Element | null { const { i18n, t } = useTranslation(['app_settings', 'shared', 'branded']) const enableLocalization = useFeatureFlag('enableLocalization') const [currentOption, setCurrentOption] = useState( - languageOptions[0] + LANGUAGES[0] ) const dispatch = useDispatch() @@ -76,7 +71,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { } const handleDropdownClick = (value: string): void => { - const selectedOption = languageOptions.find(lng => lng.value === value) + const selectedOption = LANGUAGES.find(lng => lng.value === value) if (selectedOption != null) { setCurrentOption(selectedOption) @@ -89,8 +84,8 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { if (systemLanguage != null) { // prefer match entire locale, then match just language e.g. zh-Hant and zh-CN const matchedSystemLanguageOption = - languageOptions.find(lng => lng.value === systemLanguage) ?? - languageOptions.find( + LANGUAGES.find(lng => lng.value === systemLanguage) ?? + LANGUAGES.find( lng => new Intl.Locale(lng.value).language === new Intl.Locale(systemLanguage).language @@ -115,7 +110,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { {showBootModal ? ( { const mockFailedCommand = { @@ -41,6 +45,9 @@ describe('useRecoveryCommands', () => { const mockResumeRunFromRecovery = vi.fn(() => Promise.resolve(mockMakeSuccessToast()) ) + const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn(() => + Promise.resolve(mockMakeSuccessToast()) + ) const mockStopRun = vi.fn() const mockChainRunCommands = vi.fn().mockResolvedValue([]) const mockReportActionSelectedResult = vi.fn() @@ -73,6 +80,11 @@ describe('useRecoveryCommands', () => { vi.mocked(useUpdateErrorRecoveryPolicy).mockReturnValue({ mutateAsync: mockUpdateErrorRecoveryPolicy, } as any) + vi.mocked( + useResumeRunFromRecoveryAssumingFalsePositiveMutation + ).mockReturnValue({ + mutateAsync: mockResumeRunFromRecoveryAssumingFalsePositive, + } as any) }) it('should call chainRunRecoveryCommands with continuePastCommandFailure set to false', async () => { @@ -317,7 +329,8 @@ describe('useRecoveryCommands', () => { const expectedPolicyRules = buildIgnorePolicyRules( 'aspirateInPlace', - 'mockErrorType' + 'mockErrorType', + 'ignoreAndContinue' ) expect(mockUpdateErrorRecoveryPolicy).toHaveBeenCalledWith( @@ -354,4 +367,54 @@ describe('useRecoveryCommands', () => { RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE ) }) + + describe('skipFailedCommand with false positive handling', () => { + it('should call resumeRunFromRecoveryAssumingFalsePositive for tip-related errors', async () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.TIP_NOT_DETECTED) + + const { result } = renderHook(() => useRecoveryCommands(props)) + + await act(async () => { + await result.current.skipFailedCommand() + }) + + expect( + mockResumeRunFromRecoveryAssumingFalsePositive + ).toHaveBeenCalledWith(mockRunId) + expect(mockMakeSuccessToast).toHaveBeenCalled() + }) + + it('should call regular resumeRunFromRecovery for non-tip-related errors', async () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.GRIPPER_ERROR) + + const { result } = renderHook(() => useRecoveryCommands(props)) + + await act(async () => { + await result.current.skipFailedCommand() + }) + + expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(mockRunId) + expect(mockMakeSuccessToast).toHaveBeenCalled() + }) + }) +}) + +describe('isAssumeFalsePositiveResumeKind', () => { + it(`should return true for ${ERROR_KINDS.TIP_NOT_DETECTED} error kind`, () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.TIP_NOT_DETECTED) + + expect(isAssumeFalsePositiveResumeKind({} as any)).toBe(true) + }) + + it(`should return true for ${ERROR_KINDS.TIP_DROP_FAILED} error kind`, () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.TIP_DROP_FAILED) + + expect(isAssumeFalsePositiveResumeKind({} as any)).toBe(true) + }) + + it('should return false for other error kinds', () => { + vi.mocked(getErrorKind).mockReturnValue(ERROR_KINDS.GRIPPER_ERROR) + + expect(isAssumeFalsePositiveResumeKind({} as any)).toBe(false) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index 69101d92fe9..7614dec4be3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -5,10 +5,12 @@ import { useResumeRunFromRecoveryMutation, useStopRunMutation, useUpdateErrorRecoveryPolicy, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '@opentrons/react-api-client' import { useChainRunCommands } from '/app/resources/runs' -import { RECOVERY_MAP } from '../constants' +import { ERROR_KINDS, RECOVERY_MAP } from '../constants' +import { getErrorKind } from '/app/organisms/ErrorRecoveryFlows/utils' import type { CreateCommand, @@ -23,7 +25,9 @@ import type { } from '@opentrons/shared-data' import type { CommandData, + IfMatchType, RecoveryPolicyRulesParams, + RunAction, } from '@opentrons/api-client' import type { WellGroup } from '@opentrons/components' import type { FailedCommand, RecoveryRoute, RouteStep } from '../types' @@ -89,6 +93,9 @@ export function useRecoveryCommands({ const { mutateAsync: resumeRunFromRecovery, } = useResumeRunFromRecoveryMutation() + const { + mutateAsync: resumeRunFromRecoveryAssumingFalsePositive, + } = useResumeRunFromRecoveryAssumingFalsePositiveMutation() const { stopRun } = useStopRunMutation() const { mutateAsync: updateErrorRecoveryPolicy, @@ -198,9 +205,16 @@ export function useRecoveryCommands({ const handleIgnoringErrorKind = useCallback((): Promise => { if (ignoreErrors) { if (failedCommandByRunRecord?.error != null) { + const ifMatch: IfMatchType = isAssumeFalsePositiveResumeKind( + failedCommandByRunRecord + ) + ? 'assumeFalsePositiveAndContinue' + : 'ignoreAndContinue' + const ignorePolicyRules = buildIgnorePolicyRules( failedCommandByRunRecord.commandType, - failedCommandByRunRecord.error.errorType + failedCommandByRunRecord.error.errorType, + ifMatch ) return updateErrorRecoveryPolicy(ignorePolicyRules) @@ -247,9 +261,17 @@ export function useRecoveryCommands({ stopRun(runId) }, [runId]) + const handleResumeAction = (): Promise => { + if (isAssumeFalsePositiveResumeKind(failedCommandByRunRecord)) { + return resumeRunFromRecoveryAssumingFalsePositive(runId) + } else { + return resumeRunFromRecovery(runId) + } + } + const skipFailedCommand = useCallback((): void => { void handleIgnoringErrorKind().then(() => - resumeRunFromRecovery(runId).then(() => { + handleResumeAction().then(() => { analytics.reportActionSelectedResult( selectedRecoveryOption, 'succeeded' @@ -303,6 +325,20 @@ export function useRecoveryCommands({ } } +export function isAssumeFalsePositiveResumeKind( + failedCommandByRunRecord: UseRecoveryCommandsParams['failedCommandByRunRecord'] +): boolean { + const errorKind = getErrorKind(failedCommandByRunRecord) + + switch (errorKind) { + case ERROR_KINDS.TIP_NOT_DETECTED: + case ERROR_KINDS.TIP_DROP_FAILED: + return true + default: + return false + } +} + export const HOME_PIPETTE_Z_AXES: CreateCommand = { commandType: 'home', params: { axes: ['leftZ', 'rightZ'] }, @@ -372,13 +408,14 @@ export const buildPickUpTips = ( export const buildIgnorePolicyRules = ( commandType: FailedCommand['commandType'], - errorType: string + errorType: string, + ifMatch: IfMatchType ): RecoveryPolicyRulesParams => { return [ { commandType, errorType, - ifMatch: 'ignoreAndContinue', + ifMatch, }, ] } diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx new file mode 100644 index 00000000000..a935a5571ad --- /dev/null +++ b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components' + +import { + BORDERS, + COLORS, + CURSOR_POINTER, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' + +import { LANGUAGES } from '/app/i18n' +import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import { getAppLanguage, updateConfigValue } from '/app/redux/config' + +import type { Dispatch } from '/app/redux/types' +import type { SetSettingOption } from './types' + +interface LabelProps { + isSelected?: boolean +} + +const SettingButton = styled.input` + display: none; +` + +const SettingButtonLabel = styled.label` + padding: ${SPACING.spacing24}; + border-radius: ${BORDERS.borderRadius16}; + cursor: ${CURSOR_POINTER}; + background: ${({ isSelected }) => + isSelected === true ? COLORS.blue50 : COLORS.blue35}; + color: ${({ isSelected }) => isSelected === true && COLORS.white}; +` + +interface LanguageSettingProps { + setCurrentOption: SetSettingOption +} + +export function LanguageSetting({ + setCurrentOption, +}: LanguageSettingProps): JSX.Element { + const { t } = useTranslation('app_settings') + const dispatch = useDispatch() + + const appLanguage = useSelector(getAppLanguage) + + const handleChange = (event: React.ChangeEvent): void => { + dispatch(updateConfigValue('language.appLanguage', event.target.value)) + } + + return ( + + { + setCurrentOption(null) + }} + /> + + {LANGUAGES.map(lng => ( + + + + + {lng.name} + + + + ))} + + + ) +} diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx new file mode 100644 index 00000000000..80d35ebea15 --- /dev/null +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx @@ -0,0 +1,60 @@ +import type * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' + +import { + i18n, + US_ENGLISH_DISPLAY_NAME, + US_ENGLISH, + SIMPLIFIED_CHINESE_DISPLAY_NAME, + SIMPLIFIED_CHINESE, +} from '/app/i18n' +import { getAppLanguage, updateConfigValue } from '/app/redux/config' +import { renderWithProviders } from '/app/__testing-utils__' + +import { LanguageSetting } from '../LanguageSetting' + +vi.mock('/app/redux/config') + +const mockSetCurrentOption = vi.fn() + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('LanguageSetting', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + setCurrentOption: mockSetCurrentOption, + } + vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) + }) + + it('should render text and buttons', () => { + render(props) + screen.getByText('Language') + screen.getByText(US_ENGLISH_DISPLAY_NAME) + screen.getByText(SIMPLIFIED_CHINESE_DISPLAY_NAME) + }) + + it('should call mock function when tapping a language button', () => { + render(props) + const button = screen.getByText(SIMPLIFIED_CHINESE_DISPLAY_NAME) + fireEvent.click(button) + expect(updateConfigValue).toHaveBeenCalledWith( + 'language.appLanguage', + SIMPLIFIED_CHINESE + ) + }) + + it('should call mock function when tapping back button', () => { + render(props) + const button = screen.getByRole('button') + fireEvent.click(button) + expect(props.setCurrentOption).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/index.ts b/app/src/organisms/ODD/RobotSettingsDashboard/index.ts index 30933095135..a468c86829b 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/index.ts +++ b/app/src/organisms/ODD/RobotSettingsDashboard/index.ts @@ -1,4 +1,5 @@ export * from './DeviceReset' +export * from './LanguageSetting' export * from './NetworkSettings/RobotSettingsJoinOtherNetwork' export * from './NetworkSettings/RobotSettingsSelectAuthenticationType' export * from './NetworkSettings/RobotSettingsSetWifiCred' diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/types.ts b/app/src/organisms/ODD/RobotSettingsDashboard/types.ts index 231d26c837b..78e1f552daa 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/types.ts +++ b/app/src/organisms/ODD/RobotSettingsDashboard/types.ts @@ -17,5 +17,6 @@ export type SettingOption = | 'RobotSettingsSetWifiCred' | 'RobotSettingsWifi' | 'RobotSettingsWifiConnect' + | 'LanguageSetting' export type SetSettingOption = (option: SettingOption | null) => void diff --git a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx index 7450fb34e4e..2ba0d50ea3b 100644 --- a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx +++ b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx @@ -40,16 +40,19 @@ describe('useRunControls hook', () => { const mockStopRun = vi.fn() const mockCloneRun = vi.fn() const mockResumeRunFromRecovery = vi.fn() + const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn() when(useRunActionMutations).calledWith(mockPausedRun.id).thenReturn({ playRun: mockPlayRun, pauseRun: mockPauseRun, stopRun: mockStopRun, resumeRunFromRecovery: mockResumeRunFromRecovery, + resumeRunFromRecoveryAssumingFalsePositive: mockResumeRunFromRecoveryAssumingFalsePositive, isPlayRunActionLoading: false, isPauseRunActionLoading: false, isStopRunActionLoading: false, isResumeRunFromRecoveryActionLoading: false, + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading: false, }) when(useCloneRun).calledWith(mockPausedRun.id, undefined, true).thenReturn({ cloneRun: mockCloneRun, diff --git a/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx b/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx index 4eda66f68e1..e8f3724299b 100644 --- a/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx +++ b/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx @@ -1,16 +1,4 @@ -import { useContext } from 'react' -import { I18nContext } from 'react-i18next' -import { useDispatch } from 'react-redux' -import { css } from 'styled-components' - -import { - Box, - DIRECTION_COLUMN, - Flex, - RadioGroup, - SPACING, - TYPOGRAPHY, -} from '@opentrons/components' +import { Box, SPACING } from '@opentrons/components' import { Divider } from '/app/atoms/structure' import { @@ -25,9 +13,6 @@ import { UpdatedChannel, AdditionalCustomLabwareSourceFolder, } from '/app/organisms/Desktop/AdvancedSettings' -import { updateConfigValue, useFeatureFlag } from '/app/redux/config' - -import type { Dispatch } from '/app/redux/types' export function AdvancedSettings(): JSX.Element { return ( @@ -52,44 +37,7 @@ export function AdvancedSettings(): JSX.Element { - {/* TODO(bh, 2024-09-23): remove when localization setting designs implemented */} - ) } - -function LocalizationSetting(): JSX.Element | null { - const enableLocalization = useFeatureFlag('enableLocalization') - const dispatch = useDispatch() - - const { i18n } = useContext(I18nContext) - - return enableLocalization ? ( - <> - - - ) => { - dispatch( - updateConfigValue( - 'language.appLanguage', - event.currentTarget.value - ) - ) - }} - options={[ - { name: 'EN', value: 'en' }, - { name: 'CN', value: 'zh' }, - ]} - /> - - - ) : null -} diff --git a/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx b/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx index 82960177c9b..db948403fd0 100644 --- a/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx +++ b/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx @@ -12,6 +12,7 @@ import { COLORS, DIRECTION_COLUMN, DIRECTION_ROW, + DropdownMenu, Flex, JUSTIFY_SPACE_BETWEEN, Link, @@ -25,6 +26,7 @@ import { import { TertiaryButton, ToggleButton } from '/app/atoms/buttons' import { ExternalLink } from '/app/atoms/Link/ExternalLink' import { Divider } from '/app/atoms/structure' +import { LANGUAGES } from '/app/i18n' import { CURRENT_VERSION, getAvailableShellUpdate, @@ -40,6 +42,11 @@ import { useTrackEvent, ANALYTICS_APP_UPDATE_NOTIFICATIONS_TOGGLED, } from '/app/redux/analytics' +import { + getAppLanguage, + updateConfigValue, + useFeatureFlag, +} from '/app/redux/config' import { UpdateAppModal } from '/app/organisms/Desktop/UpdateAppModal' import { PreviousVersionModal } from '/app/organisms/Desktop/AppSettings/PreviousVersionModal' import { ConnectRobotSlideout } from '/app/organisms/Desktop/AppSettings/ConnectRobotSlideout' @@ -62,6 +69,15 @@ export function GeneralSettings(): JSX.Element { setShowPreviousVersionModal, ] = useState(false) const updateAvailable = Boolean(useSelector(getAvailableShellUpdate)) + + const enableLocalization = useFeatureFlag('enableLocalization') + const appLanguage = useSelector(getAppLanguage) + const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) + + const handleDropdownClick = (value: string): void => { + dispatch(updateConfigValue('language.appLanguage', value)) + } + const [showUpdateBanner, setShowUpdateBanner] = useState( updateAvailable ) @@ -260,6 +276,35 @@ export function GeneralSettings(): JSX.Element { {t('setup_connection')} + + {enableLocalization && currentLanguageOption != null ? ( + <> + + + + {t('app_language_preferences')} + + + {t('app_language_description')} + + + + + + + ) : null} {showUpdateModal ? createPortal( diff --git a/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx b/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx index 5cfd02e09a8..539c5899e8a 100644 --- a/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx +++ b/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx @@ -1,11 +1,23 @@ import { MemoryRouter } from 'react-router-dom' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' -import { screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' +import { when } from 'vitest-when' import { renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' +import { + i18n, + SIMPLIFIED_CHINESE, + SIMPLIFIED_CHINESE_DISPLAY_NAME, + US_ENGLISH, + US_ENGLISH_DISPLAY_NAME, +} from '/app/i18n' import { getAlertIsPermanentlyIgnored } from '/app/redux/alerts' +import { + getAppLanguage, + updateConfigValue, + useFeatureFlag, +} from '/app/redux/config' import * as Shell from '/app/redux/shell' import { GeneralSettings } from '../GeneralSettings' @@ -29,34 +41,38 @@ describe('GeneralSettings', () => { beforeEach(() => { vi.mocked(Shell.getAvailableShellUpdate).mockReturnValue(null) vi.mocked(getAlertIsPermanentlyIgnored).mockReturnValue(false) + vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) + when(vi.mocked(useFeatureFlag)) + .calledWith('enableLocalization') + .thenReturn(true) }) afterEach(() => { vi.resetAllMocks() }) it('renders correct titles', () => { - const [{ getByText }] = render() - getByText('App Software Version') - getByText('Software Update Alerts') - getByText('Connect to a Robot via IP Address') + render() + screen.getByText('App Software Version') + screen.getByText('Software Update Alerts') + screen.getByText('Connect to a Robot via IP Address') }) it('renders software version section with no update available', () => { - const [{ getByText, getByRole }] = render() - getByText('Up to date') - getByText('View latest release notes on') - expect(getByRole('link', { name: 'GitHub' })).toHaveAttribute( + render() + screen.getByText('Up to date') + screen.getByText('View latest release notes on') + expect(screen.getByRole('link', { name: 'GitHub' })).toHaveAttribute( 'href', 'https://github.com/Opentrons/opentrons/blob/edge/app-shell/build/release-notes.md' ) - getByRole('button', { + screen.getByRole('button', { name: 'See how to restore a previous software version', }) expect( 'It is very important for the robot and app software to be on the same version. Manage the robot software versions via Robot Settings > Advanced.' ).toBeTruthy() expect( - getByRole('link', { + screen.getByRole('link', { name: 'Learn more about keeping the Opentrons App and robot software in sync', }) @@ -65,8 +81,8 @@ describe('GeneralSettings', () => { it('renders correct info if there is update available', () => { vi.mocked(Shell.getAvailableShellUpdate).mockReturnValue('5.0.0-beta.8') - const [{ getByRole }] = render() - getByRole('button', { name: 'View software update' }) + render() + screen.getByRole('button', { name: 'View software update' }) }) it('renders correct info if there is no update available', () => { @@ -80,17 +96,35 @@ describe('GeneralSettings', () => { }) it('renders the text and toggle for update alert section', () => { - const [{ getByText, getByRole }] = render() - getByText( + render() + screen.getByText( 'Receive an alert when an Opentrons software update is available.' ) - getByRole('switch', { + screen.getByRole('switch', { name: 'Enable app update notifications', }) }) it('renders the ip address button', () => { - const [{ getByRole }] = render() - getByRole('button', { name: 'Set up connection' }) + render() + screen.getByRole('button', { name: 'Set up connection' }) + }) + + it('renders the text and dropdown for the app language preferences section', () => { + render() + screen.getByText('App Language Preferences') + screen.getByText( + 'All app features use this language. Protocols and other user content will not change language.' + ) + fireEvent.click(screen.getByText(US_ENGLISH_DISPLAY_NAME)) + fireEvent.click( + screen.getByRole('button', { + name: SIMPLIFIED_CHINESE_DISPLAY_NAME, + }) + ) + expect(updateConfigValue).toBeCalledWith( + 'language.appLanguage', + SIMPLIFIED_CHINESE + ) }) }) diff --git a/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx b/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx new file mode 100644 index 00000000000..8508a7b4d08 --- /dev/null +++ b/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx @@ -0,0 +1,59 @@ +import { vi, it, describe, expect } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { updateConfigValue } from '/app/redux/config' +import { ChooseLanguage } from '..' + +import type { NavigateFunction } from 'react-router-dom' + +const mockNavigate = vi.fn() +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useNavigate: () => mockNavigate, + } +}) +vi.mock('/app/redux/config') + +const render = () => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + ) +} + +describe('ChooseLanguage', () => { + it('should render text, language options, and continue button', () => { + render() + screen.getByText('Choose your language') + screen.getByText('Select a language to personalize your experience.') + screen.getByRole('label', { name: 'English (US)' }) + screen.getByRole('label', { name: '中文' }) + screen.getByRole('button', { name: 'Continue' }) + }) + + it('should initialize english', () => { + render() + expect(updateConfigValue).toBeCalledWith('language.appLanguage', 'en-US') + }) + + it('should change language when language option selected', () => { + render() + fireEvent.click(screen.getByRole('label', { name: '中文' })) + expect(updateConfigValue).toBeCalledWith('language.appLanguage', 'zh-CN') + }) + + it('should call mockNavigate when tapping continue', () => { + render() + fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + expect(mockNavigate).toHaveBeenCalledWith('/welcome') + }) +}) diff --git a/app/src/pages/ODD/ChooseLanguage/index.tsx b/app/src/pages/ODD/ChooseLanguage/index.tsx new file mode 100644 index 00000000000..d0110e68591 --- /dev/null +++ b/app/src/pages/ODD/ChooseLanguage/index.tsx @@ -0,0 +1,79 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' + +import { + DIRECTION_COLUMN, + Flex, + JUSTIFY_SPACE_BETWEEN, + RadioButton, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { MediumButton } from '/app/atoms/buttons' +import { LANGUAGES, US_ENGLISH } from '/app/i18n' +import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader' +import { getAppLanguage, updateConfigValue } from '/app/redux/config' + +import type { Dispatch } from '/app/redux/types' + +export function ChooseLanguage(): JSX.Element { + const { i18n, t } = useTranslation(['app_settings', 'shared']) + const navigate = useNavigate() + const dispatch = useDispatch() + + const appLanguage = useSelector(getAppLanguage) + + useEffect(() => { + // initialize en-US language on mount + dispatch(updateConfigValue('language.appLanguage', US_ENGLISH)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + + + + + + {t('select_a_language')} + + + {LANGUAGES.map(lng => ( + { + dispatch(updateConfigValue('language.appLanguage', lng.value)) + }} + > + ))} + + + { + navigate('/welcome') + }} + width="100%" + /> + + + ) +} diff --git a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx index a649a65e40c..043b2b8b843 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx @@ -1,6 +1,5 @@ -import { useContext } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { I18nContext, useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { @@ -21,18 +20,19 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import { LANGUAGES } from '/app/i18n' import { getLocalRobot, getRobotApiVersion } from '/app/redux/discovery' import { getRobotUpdateAvailable } from '/app/redux/robot-update' import { useErrorRecoverySettingsToggle } from '/app/resources/errorRecovery' import { DEV_INTERNAL_FLAGS, + getAppLanguage, getApplyHistoricOffsets, getDevtoolsEnabled, getFeatureFlags, toggleDevInternalFlag, toggleDevtools, toggleHistoricOffsets, - updateConfigValue, useFeatureFlag, } from '/app/redux/config' import { InlineNotification } from '/app/atoms/InlineNotification' @@ -88,6 +88,10 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { const { lightsEnabled, toggleLights } = useLEDLights(robotName) const { toggleERSettings, isEREnabled } = useErrorRecoverySettingsToggle() + const appLanguage = useSelector(getAppLanguage) + const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) + const enableLocalization = useFeatureFlag('enableLocalization') + return ( @@ -139,6 +143,18 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { } /> + {enableLocalization ? ( + { + setCurrentOption('LanguageSetting') + }} + iconName="language" + /> + ) : null} dispatch(toggleDevtools())} /> {devToolsOn ? : null} - {/* TODO(bh, 2024-09-23): remove when localization setting designs implemented */} - ) @@ -282,22 +296,3 @@ function FeatureFlags(): JSX.Element { ) } - -function LanguageToggle(): JSX.Element | null { - const enableLocalization = useFeatureFlag('enableLocalization') - const dispatch = useDispatch() - - const { i18n } = useContext(I18nContext) - - return enableLocalization ? ( - { - i18n.language === 'en' - ? dispatch(updateConfigValue('language.appLanguage', 'zh')) - : dispatch(updateConfigValue('language.appLanguage', 'en')) - }} - rightElement={<>} - /> - ) : null -} diff --git a/app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx b/app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx index 07fdb119ee4..00b70120809 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx @@ -1,19 +1,26 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' +import { when } from 'vitest-when' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { getRobotSettings } from '/app/redux/robot-settings' import { getLocalRobot } from '/app/redux/discovery' -import { toggleDevtools, toggleHistoricOffsets } from '/app/redux/config' +import { + getAppLanguage, + toggleDevtools, + toggleHistoricOffsets, + useFeatureFlag, +} from '/app/redux/config' import { mockConnectedRobot } from '/app/redux/discovery/__fixtures__' import { Navigation } from '/app/organisms/ODD/Navigation' import { DeviceReset, TouchScreenSleep, TouchscreenBrightness, + LanguageSetting, NetworkSettings, Privacy, RobotSystemVersion, @@ -44,6 +51,7 @@ vi.mock('/app/organisms/ODD/RobotSettingsDashboard/RobotSystemVersion') vi.mock('/app/organisms/ODD/RobotSettingsDashboard/TouchscreenBrightness') vi.mock('/app/organisms/ODD/RobotSettingsDashboard/UpdateChannel') vi.mock('/app/organisms/ODD/RobotSettingsDashboard/Privacy') +vi.mock('/app/organisms/ODD/RobotSettingsDashboard/LanguageSetting') const mockToggleLights = vi.fn() const mockToggleER = vi.fn() @@ -59,6 +67,8 @@ const render = () => { ) } +const MOCK_DEFAULT_LANGUAGE = 'en-US' + // Note kj 01/25/2023 Currently test cases only check text since this PR is bare-bones for RobotSettings Dashboard describe('RobotSettingsDashboard', () => { beforeEach(() => { @@ -81,6 +91,10 @@ describe('RobotSettingsDashboard', () => { isEREnabled: true, toggleERSettings: mockToggleER, }) + vi.mocked(getAppLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) + when(vi.mocked(useFeatureFlag)) + .calledWith('enableLocalization') + .thenReturn(true) }) afterEach(() => { @@ -249,4 +263,13 @@ describe('RobotSettingsDashboard', () => { render() screen.getByText('Update available') }) + + it('should render component when tapping Language', () => { + render() + + screen.getByText('English (US)') + const button = screen.getByText('Language') + fireEvent.click(button) + expect(vi.mocked(LanguageSetting)).toHaveBeenCalled() + }) }) diff --git a/app/src/pages/ODD/RobotSettingsDashboard/index.tsx b/app/src/pages/ODD/RobotSettingsDashboard/index.tsx index 30925f1ae44..401c4815aac 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/index.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/index.tsx @@ -8,6 +8,7 @@ import { DeviceReset, TouchscreenBrightness, TouchScreenSleep, + LanguageSetting, NetworkSettings, Privacy, RobotName, @@ -200,6 +201,9 @@ export function RobotSettingsDashboard(): JSX.Element { /> ) + case 'LanguageSetting': + return + // fallthrough option: render the robot settings list of buttons default: return diff --git a/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx index 2605c1bad5b..f98666d3cbd 100644 --- a/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -76,6 +76,7 @@ const mockPlayRun = vi.fn() const mockPauseRun = vi.fn() const mockStopRun = vi.fn() const mockResumeRunFromRecovery = vi.fn() +const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn() const render = (path = '/') => { return renderWithProviders( @@ -133,10 +134,12 @@ describe('RunningProtocol', () => { pauseRun: mockPauseRun, stopRun: mockStopRun, resumeRunFromRecovery: mockResumeRunFromRecovery, + resumeRunFromRecoveryAssumingFalsePositive: mockResumeRunFromRecoveryAssumingFalsePositive, isPlayRunActionLoading: false, isPauseRunActionLoading: false, isStopRunActionLoading: false, isResumeRunFromRecoveryActionLoading: false, + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading: false, }) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) diff --git a/app/src/redux/analytics/__tests__/make-event.test.ts b/app/src/redux/analytics/__tests__/make-event.test.ts index bd938292d5a..70506dc162a 100644 --- a/app/src/redux/analytics/__tests__/make-event.test.ts +++ b/app/src/redux/analytics/__tests__/make-event.test.ts @@ -121,4 +121,23 @@ describe('analytics events map', () => { }) }) }) + + describe('events with calibration data', () => { + it('analytics:RESOURCE_MONITOR_REPORT -> resourceMonitorReport event', () => { + const state = {} as any + const action = { + type: 'analytics:RESOURCE_MONITOR_REPORT', + payload: { + systemAvailMemMb: '500', + systemUptimeHrs: '111', + processesDetails: [], + }, + } as any + + return expect(makeEvent(action, state)).resolves.toEqual({ + name: 'resourceMonitorReport', + properties: { ...action.payload }, + }) + }) + }) }) diff --git a/app/src/redux/analytics/constants.ts b/app/src/redux/analytics/constants.ts index cf99bfad9ea..cde9b0a1d59 100644 --- a/app/src/redux/analytics/constants.ts +++ b/app/src/redux/analytics/constants.ts @@ -97,3 +97,9 @@ export const ANALYTICS_QUICK_TRANSFER_DETAILS_PAGE = 'quickTransferDetailsPage' export const ANALYTICS_QUICK_TRANSFER_RUN_FROM_DETAILS = 'quickTransferRunFromDetails' export const ANALYTICS_QUICK_TRANSFER_RERUN = 'quickTransferReRunFromSummary' + +/** + * Resource Monitor Analytics + */ +export const ANALYTICS_RESOURCE_MONITOR_REPORT: 'analytics:RESOURCE_MONITOR_REPORT' = + 'analytics:RESOURCE_MONITOR_REPORT' diff --git a/app/src/redux/analytics/make-event.ts b/app/src/redux/analytics/make-event.ts index da3a812fbdc..bc5c8955104 100644 --- a/app/src/redux/analytics/make-event.ts +++ b/app/src/redux/analytics/make-event.ts @@ -247,6 +247,15 @@ export function makeEvent( }) } + case Constants.ANALYTICS_RESOURCE_MONITOR_REPORT: { + return Promise.resolve({ + name: 'resourceMonitorReport', + properties: { + ...action.payload, + }, + }) + } + case RobotAdmin.RESET_CONFIG: { const { resets } = action.payload return Promise.resolve({ diff --git a/app/src/redux/analytics/mixpanel.ts b/app/src/redux/analytics/mixpanel.ts index 20a5a2ed170..aa5ad5a7893 100644 --- a/app/src/redux/analytics/mixpanel.ts +++ b/app/src/redux/analytics/mixpanel.ts @@ -43,9 +43,12 @@ export function trackEvent( log.debug('Trackable event', { event, optedIn }) if (MIXPANEL_ID != null && optedIn) { - if (event.superProperties != null) mixpanel.register(event.superProperties) - if ('name' in event && event.name != null) + if (event.superProperties != null) { + mixpanel.register(event.superProperties) + } + if ('name' in event && event.name != null) { mixpanel.track(event.name, event.properties) + } } } diff --git a/app/src/redux/analytics/types.ts b/app/src/redux/analytics/types.ts index dfb3e374ad9..d27c2955fe2 100644 --- a/app/src/redux/analytics/types.ts +++ b/app/src/redux/analytics/types.ts @@ -5,6 +5,7 @@ import type { Config } from '../config/types' import type { ANALYTICS_PIPETTE_OFFSET_STARTED, ANALYTICS_TIP_LENGTH_STARTED, + ANALYTICS_RESOURCE_MONITOR_REPORT, } from './constants' export type AnalyticsConfig = Config['analytics'] @@ -118,9 +119,19 @@ export interface TipLengthStartedAnalyticsAction { } } +export interface ResourceMonitorAnalyticsAction { + type: typeof ANALYTICS_RESOURCE_MONITOR_REPORT + payload: { + systemAvailMemMb: string + systemUptimeHrs: string + processesDetails: Array> + } +} + export type AnalyticsTriggerAction = | PipetteOffsetStartedAnalyticsAction | TipLengthStartedAnalyticsAction + | ResourceMonitorAnalyticsAction export interface SessionInstrumentAnalyticsData { sessionType: string diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index ce4af4ddaeb..a38ed931ec9 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -1,4 +1,5 @@ import type { LogLevel } from '../../logger' +import type { Language } from '/app/i18n' import type { ProtocolSort } from '/app/redux/protocol-storage' export type UrlProtocol = 'file:' | 'http:' @@ -31,8 +32,6 @@ export type QuickTransfersOnDeviceSortKey = | 'recentCreated' | 'oldCreated' -export type Language = 'en-US' | 'zh-CN' - export interface OnDeviceDisplaySettings { sleepMs: number brightness: number diff --git a/app/src/redux/config/selectors.ts b/app/src/redux/config/selectors.ts index 4069aa7320b..dbac2cd3c05 100644 --- a/app/src/redux/config/selectors.ts +++ b/app/src/redux/config/selectors.ts @@ -8,8 +8,8 @@ import type { ProtocolsOnDeviceSortKey, QuickTransfersOnDeviceSortKey, OnDeviceDisplaySettings, - Language, } from './types' +import type { Language } from '/app/i18n' import type { ProtocolSort } from '/app/redux/protocol-storage' export interface SelectOption { diff --git a/components/src/icons/icon-data.ts b/components/src/icons/icon-data.ts index edf90a1512c..9f8338c7e87 100644 --- a/components/src/icons/icon-data.ts +++ b/components/src/icons/icon-data.ts @@ -1,5 +1,8 @@ // icon data -export const ICON_DATA_BY_NAME = { +export const ICON_DATA_BY_NAME: Record< + string, + { path: string; viewBox: string } +> = { add: { path: 'M24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.2548 10.7452 48 24 48ZM21 21V12H27V21H36V27H27V36H21V27H12V21H21Z', @@ -272,6 +275,11 @@ export const ICON_DATA_BY_NAME = { 'M8.63355 14.215C8.6365 14.5124 8.63911 14.7764 8.63911 15H7.36528C7.36528 14.8447 7.36714 14.6621 7.36924 14.4568C7.38181 13.225 7.40273 11.1766 7.07922 9.31768C6.89019 8.23151 6.59339 7.27753 6.15429 6.60988C5.73178 5.96745 5.2075 5.625 4.50024 5.625C3.79297 5.625 3.2687 5.96745 2.84618 6.60988C2.40708 7.27753 2.11028 8.23151 1.92125 9.31768C1.59774 11.1766 1.61866 13.225 1.63124 14.4568C1.63333 14.6621 1.6352 14.8447 1.6352 15H0.385197C0.385197 14.8588 0.383339 14.6876 0.38121 14.4914C0.367987 13.273 0.344279 11.0886 0.689764 9.10337C0.890012 7.95271 1.2241 6.80142 1.80181 5.92302C2.39611 5.01939 3.27456 4.375 4.50024 4.375C5.72592 4.375 6.60437 5.01939 7.19867 5.92301C7.40983 6.24409 7.58845 6.60162 7.73987 6.98226C7.75025 6.91343 7.76092 6.84476 7.77188 6.77626C8.02647 5.18496 8.4487 3.62176 9.16765 2.44065C9.89976 1.23791 10.9823 0.375 12.5141 0.375C14.0321 0.375 15.1148 1.19161 15.852 2.35243C16.5736 3.48863 16.9954 4.99227 17.2488 6.52295C17.6344 8.85309 17.6513 11.4038 17.6372 13.1367L19.1208 12.0031L19.8797 12.9963L17.0169 15.1838L14.1235 12.9984L14.8769 12.001L16.3866 13.1413C16.4006 11.422 16.3863 8.96723 16.0155 6.72705C15.7724 5.25773 15.3851 3.94887 14.7968 3.02257C14.2242 2.12089 13.4961 1.625 12.5141 1.625C11.5458 1.625 10.8158 2.13709 10.2354 3.0906C9.64181 4.06574 9.25156 5.44004 9.00618 6.97374C8.58775 9.58904 8.61631 12.4739 8.63355 14.215Z', viewBox: '0 0 20 16', }, + language: { + path: + 'M10 18.3333C8.83335 18.3333 7.74308 18.1146 6.72919 17.6771C5.7153 17.2396 4.83335 16.6458 4.08335 15.8958C3.33335 15.1458 2.74308 14.2604 2.31252 13.2396C1.88196 12.2187 1.66669 11.125 1.66669 9.95832C1.66669 8.79166 1.88196 7.70485 2.31252 6.69791C2.74308 5.69096 3.33335 4.81249 4.08335 4.06249C4.83335 3.31249 5.7153 2.72568 6.72919 2.30207C7.74308 1.87846 8.83335 1.66666 10 1.66666C11.1667 1.66666 12.257 1.87846 13.2709 2.30207C14.2847 2.72568 15.1667 3.31249 15.9167 4.06249C16.6667 4.81249 17.257 5.69096 17.6875 6.69791C18.1181 7.70485 18.3334 8.79166 18.3334 9.95832C18.3334 11.125 18.1181 12.2187 17.6875 13.2396C17.257 14.2604 16.6667 15.1458 15.9167 15.8958C15.1667 16.6458 14.2847 17.2396 13.2709 17.6771C12.257 18.1146 11.1667 18.3333 10 18.3333ZM10 17.125C10.4861 16.625 10.8924 16.0521 11.2188 15.4062C11.5452 14.7604 11.8125 13.993 12.0209 13.1042H8.00002C8.19446 13.9375 8.45488 14.6875 8.78127 15.3542C9.10766 16.0208 9.51391 16.6111 10 17.125ZM8.22919 16.875C7.88196 16.3472 7.58335 15.7778 7.33335 15.1667C7.08335 14.5555 6.87502 13.868 6.70835 13.1042H3.58335C4.11113 14.0903 4.72224 14.8646 5.41669 15.4271C6.11113 15.9896 7.04863 16.4722 8.22919 16.875ZM11.7917 16.8542C12.7917 16.5347 13.691 16.0555 14.4896 15.4167C15.2882 14.7778 15.9306 14.0069 16.4167 13.1042H13.3125C13.132 13.8542 12.9202 14.5347 12.6771 15.1458C12.434 15.7569 12.1389 16.3264 11.7917 16.8542ZM3.16669 11.8542H6.47919C6.43752 11.4792 6.41321 11.1424 6.40627 10.8437C6.39933 10.5451 6.39585 10.25 6.39585 9.95832C6.39585 9.6111 6.4028 9.30207 6.41669 9.03124C6.43058 8.76041 6.45835 8.45832 6.50002 8.12499H3.16669C3.06946 8.45832 3.00349 8.75693 2.96877 9.02082C2.93405 9.28471 2.91669 9.59721 2.91669 9.95832C2.91669 10.3194 2.93405 10.6424 2.96877 10.9271C3.00349 11.2118 3.06946 11.5208 3.16669 11.8542ZM7.77085 11.8542H12.25C12.3056 11.4236 12.3403 11.0729 12.3542 10.8021C12.3681 10.5312 12.375 10.25 12.375 9.95832C12.375 9.68054 12.3681 9.41318 12.3542 9.15624C12.3403 8.89929 12.3056 8.55554 12.25 8.12499H7.77085C7.7153 8.55554 7.68058 8.89929 7.66669 9.15624C7.6528 9.41318 7.64585 9.68054 7.64585 9.95832C7.64585 10.25 7.6528 10.5312 7.66669 10.8021C7.68058 11.0729 7.7153 11.4236 7.77085 11.8542ZM13.5 11.8542H16.8334C16.9306 11.5208 16.9965 11.2118 17.0313 10.9271C17.066 10.6424 17.0834 10.3194 17.0834 9.95832C17.0834 9.59721 17.066 9.28471 17.0313 9.02082C16.9965 8.75693 16.9306 8.45832 16.8334 8.12499H13.5209C13.5625 8.6111 13.5903 8.98263 13.6042 9.23957C13.6181 9.49652 13.625 9.7361 13.625 9.95832C13.625 10.2639 13.6146 10.5521 13.5938 10.8229C13.5729 11.0937 13.5417 11.4375 13.5 11.8542ZM13.2917 6.87499H16.4167C15.9584 5.91666 15.3299 5.11805 14.5313 4.47916C13.7327 3.84027 12.8125 3.38888 11.7709 3.12499C12.1181 3.63888 12.4132 4.19443 12.6563 4.79166C12.8993 5.38888 13.1111 6.08332 13.2917 6.87499ZM8.00002 6.87499H12.0417C11.8889 6.13888 11.632 5.42707 11.2709 4.73957C10.9097 4.05207 10.4861 3.44443 10 2.91666C9.55558 3.29166 9.18058 3.78471 8.87502 4.39582C8.56946 5.00693 8.2778 5.83332 8.00002 6.87499ZM3.58335 6.87499H6.72919C6.88196 6.12499 7.07641 5.45485 7.31252 4.86457C7.54863 4.2743 7.84724 3.70138 8.20835 3.14582C7.16669 3.40971 6.25696 3.85416 5.47919 4.47916C4.70141 5.10416 4.06946 5.90277 3.58335 6.87499Z', + viewBox: '0 0 20 20', + }, 'latch-closed': { path: 'M33.6663 10H6.33301V17H10.333V14H14.167V19.166H26.667V14H29.6663V17H33.6663V10Z', diff --git a/components/src/modals/ModalShell.tsx b/components/src/modals/ModalShell.tsx index 9243e6c7b72..d745d8d27a8 100644 --- a/components/src/modals/ModalShell.tsx +++ b/components/src/modals/ModalShell.tsx @@ -35,6 +35,8 @@ export interface ModalShellProps extends StyleProps { position?: Position /** Optional visible overlay */ showOverlay?: boolean + /** Optional remove padding */ + noPadding?: boolean } /** @@ -58,6 +60,7 @@ export function ModalShell(props: ModalShellProps): JSX.Element { zIndexOverlay = 1, position = 'center', showOverlay = true, + noPadding = false, ...styleProps } = props @@ -71,7 +74,7 @@ export function ModalShell(props: ModalShellProps): JSX.Element { if (onOutsideClick != null) onOutsideClick(e) }} > - + ` cursor: ${CURSOR_DEFAULT}; ` -const ContentArea = styled.div<{ zIndex: string | number; position: Position }>` +const ContentArea = styled.div<{ + zIndex: string | number + position: Position + noPadding: boolean +}>` display: flex; position: ${POSITION_ABSOLUTE}; align-items: ${({ position }) => @@ -116,7 +123,7 @@ const ContentArea = styled.div<{ zIndex: string | number; position: Position }>` width: 100%; height: 100%; z-index: ${({ zIndex }) => zIndex}; - padding: ${SPACING.spacing16}; + padding: ${({ noPadding }) => (noPadding ? 0 : SPACING.spacing16)}; ` const ModalArea = styled.div< diff --git a/opentrons-ai-client/src/OpentronsAI.test.tsx b/opentrons-ai-client/src/OpentronsAI.test.tsx index 68d604edf07..ba069ca3081 100644 --- a/opentrons-ai-client/src/OpentronsAI.test.tsx +++ b/opentrons-ai-client/src/OpentronsAI.test.tsx @@ -1,21 +1,22 @@ import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' import * as auth0 from '@auth0/auth0-react' - import { renderWithProviders } from './__testing-utils__' import { i18n } from './i18n' import { Loading } from './molecules/Loading' - import { OpentronsAI } from './OpentronsAI' import { Landing } from './pages/Landing' import { useGetAccessToken } from './resources/hooks' import { Header } from './molecules/Header' import { Footer } from './molecules/Footer' +import { HeaderWithMeter } from './molecules/HeaderWithMeter' +import { headerWithMeterAtom } from './resources/atoms' vi.mock('@auth0/auth0-react') vi.mock('./pages/Landing') vi.mock('./molecules/Header') +vi.mock('./molecules/HeaderWithMeter') vi.mock('./molecules/Footer') vi.mock('./molecules/Loading') vi.mock('./resources/hooks/useGetAccessToken') @@ -27,9 +28,14 @@ vi.mock('./resources/hooks/useTrackEvent', () => ({ useTrackEvent: () => mockUseTrackEvent, })) +const initialValues: Array<[any, any]> = [ + [headerWithMeterAtom, { displayHeaderWithMeter: false, progress: 0 }], +] + const render = (): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, + initialValues, }) } @@ -41,7 +47,14 @@ describe('OpentronsAI', () => { vi.mocked(Landing).mockReturnValue(
mock Landing page
) vi.mocked(Loading).mockReturnValue(
mock Loading
) vi.mocked(Header).mockReturnValue(
mock Header component
) + vi.mocked(HeaderWithMeter).mockReturnValue( +
mock Header With Meter component
+ ) vi.mocked(Footer).mockReturnValue(
mock Footer component
) + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: true, + isLoading: false, + }) }) it('should render loading screen when isLoading is true', () => { @@ -54,28 +67,25 @@ describe('OpentronsAI', () => { }) it('should render text', () => { - ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ - isAuthenticated: true, - isLoading: false, - }) render() screen.getByText('mock Landing page') }) - it('should render Header component', () => { - ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ - isAuthenticated: true, - isLoading: false, - }) + it('should render the default Header component if displayHeaderWithMeter is false', () => { render() + screen.getByText('mock Header component') }) + it('should render Header with meter component if displayHeaderWithMeter is true', () => { + initialValues[0][1].displayHeaderWithMeter = true + + render() + + screen.getByText('mock Header With Meter component') + }) + it('should render Footer component', () => { - ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ - isAuthenticated: true, - isLoading: false, - }) render() screen.getByText('mock Footer component') }) diff --git a/opentrons-ai-client/src/OpentronsAI.tsx b/opentrons-ai-client/src/OpentronsAI.tsx index 621c2453e50..1f91faf0eed 100644 --- a/opentrons-ai-client/src/OpentronsAI.tsx +++ b/opentrons-ai-client/src/OpentronsAI.tsx @@ -11,23 +11,24 @@ import { useAuth0 } from '@auth0/auth0-react' import { useAtom } from 'jotai' import { useEffect } from 'react' import { Loading } from './molecules/Loading' -import { mixpanelAtom, tokenAtom } from './resources/atoms' +import { headerWithMeterAtom, mixpanelAtom, tokenAtom } from './resources/atoms' import { useGetAccessToken } from './resources/hooks' import { initializeMixpanel } from './analytics/mixpanel' import { useTrackEvent } from './resources/hooks/useTrackEvent' import { Header } from './molecules/Header' import { CLIENT_MAX_WIDTH } from './resources/constants' import { Footer } from './molecules/Footer' +import { HeaderWithMeter } from './molecules/HeaderWithMeter' +import styled from 'styled-components' export function OpentronsAI(): JSX.Element | null { const { isAuthenticated, isLoading, loginWithRedirect } = useAuth0() const [, setToken] = useAtom(tokenAtom) - const [mixpanel] = useAtom(mixpanelAtom) + const [{ displayHeaderWithMeter, progress }] = useAtom(headerWithMeterAtom) + const [mixpanelState, setMixpanelState] = useAtom(mixpanelAtom) const { getAccessToken } = useGetAccessToken() const trackEvent = useTrackEvent() - initializeMixpanel(mixpanel) - const fetchAccessToken = async (): Promise => { try { const accessToken = await getAccessToken() @@ -37,6 +38,11 @@ export function OpentronsAI(): JSX.Element | null { } } + if (mixpanelState?.isInitialized === false) { + setMixpanelState({ ...mixpanelState, isInitialized: true }) + initializeMixpanel(mixpanelState) + } + useEffect(() => { if (!isAuthenticated && !isLoading) { void loginWithRedirect() @@ -61,30 +67,44 @@ export function OpentronsAI(): JSX.Element | null { } return ( -
+ + {displayHeaderWithMeter ? ( + + ) : ( +
+ )} + + -
- -
-
+ ) } + +const StickyHeader = styled.div` + position: sticky; + top: 0; + z-index: 100; +` diff --git a/opentrons-ai-client/src/OpentronsAIRoutes.tsx b/opentrons-ai-client/src/OpentronsAIRoutes.tsx index 630429c2aa1..b790b262ebe 100644 --- a/opentrons-ai-client/src/OpentronsAIRoutes.tsx +++ b/opentrons-ai-client/src/OpentronsAIRoutes.tsx @@ -2,11 +2,11 @@ import { Route, Navigate, Routes } from 'react-router-dom' import { Landing } from './pages/Landing' import type { RouteProps } from './resources/types' +import { CreateProtocol } from './pages/CreateProtocol' const opentronsAIRoutes: RouteProps[] = [ - // replace Landing with the correct component { - Component: Landing, + Component: CreateProtocol, name: 'Create A New Protocol', navLinkTo: '/new-protocol', path: '/new-protocol', diff --git a/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx index f551c875d9c..4f442fa42b9 100644 --- a/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx +++ b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx @@ -3,31 +3,49 @@ import type * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import { I18nextProvider } from 'react-i18next' -import { Provider } from 'react-redux' -import { vi } from 'vitest' import { render } from '@testing-library/react' -import { createStore } from 'redux' -import type { PreloadedState, Store } from 'redux' import type { RenderOptions, RenderResult } from '@testing-library/react' +import { useHydrateAtoms } from 'jotai/utils' +import { Provider } from 'jotai' -export interface RenderWithProvidersOptions extends RenderOptions { - initialState?: State +interface HydrateAtomsProps { + initialValues: Array<[any, any]> + children: React.ReactNode +} + +interface TestProviderProps { + initialValues: Array<[any, any]> + children: React.ReactNode +} + +const HydrateAtoms = ({ + initialValues, + children, +}: HydrateAtomsProps): React.ReactNode => { + useHydrateAtoms(initialValues) + return children +} + +export const TestProvider = ({ + initialValues, + children, +}: TestProviderProps): React.ReactNode => ( + + {children} + +) + +export interface RenderWithProvidersOptions extends RenderOptions { + initialValues?: Array<[any, any]> i18nInstance: React.ComponentProps['i18n'] } -export function renderWithProviders( +export function renderWithProviders( Component: React.ReactElement, - options?: RenderWithProvidersOptions -): [RenderResult, Store] { - const { initialState = {}, i18nInstance = null } = options ?? {} - - const store: Store = createStore( - vi.fn(), - initialState as PreloadedState - ) - store.dispatch = vi.fn() - store.getState = vi.fn(() => initialState) as () => State + options?: RenderWithProvidersOptions +): RenderResult { + const { i18nInstance = null, initialValues = [] } = options ?? {} const queryClient = new QueryClient() @@ -36,7 +54,7 @@ export function renderWithProviders( > = ({ children }) => { const BaseWrapper = ( - {children} + {children} ) if (i18nInstance != null) { @@ -48,5 +66,5 @@ export function renderWithProviders( } } - return [render(Component, { wrapper: ProviderWrapper }), store] + return render(Component, { wrapper: ProviderWrapper, ...options }) } diff --git a/opentrons-ai-client/src/analytics/mixpanel.ts b/opentrons-ai-client/src/analytics/mixpanel.ts index eb81b72e6e3..6d617dae876 100644 --- a/opentrons-ai-client/src/analytics/mixpanel.ts +++ b/opentrons-ai-client/src/analytics/mixpanel.ts @@ -1,8 +1,6 @@ import mixpanel from 'mixpanel-browser' import { getHasOptedIn } from './selectors' - -export const getIsProduction = (): boolean => - global.location.host === 'designer.opentrons.com' // UPDATE THIS TO CORRECT URL +import type { Mixpanel } from '../resources/types' export type AnalyticsEvent = | { @@ -20,7 +18,7 @@ const MIXPANEL_OPTS = { opt_out_tracking_by_default: true, } -export function initializeMixpanel(state: any): void { +export function initializeMixpanel(state: Mixpanel): void { const optedIn = getHasOptedIn(state) ?? false if (MIXPANEL_ID != null) { console.debug('Initializing Mixpanel', { optedIn }) @@ -53,7 +51,6 @@ export function setMixpanelTracking(optedIn: boolean): void { // Register "super properties" which are included with all events mixpanel.register({ appVersion: 'test', // TODO update this? - // NOTE(IL, 2020): Since PD may be in the same Mixpanel project as other OT web apps, this 'appName' property is intended to distinguish it appName: 'opentronsAIClient', }) } else { diff --git a/opentrons-ai-client/src/analytics/selectors.ts b/opentrons-ai-client/src/analytics/selectors.ts index b55165f3049..19baf7c8ec2 100644 --- a/opentrons-ai-client/src/analytics/selectors.ts +++ b/opentrons-ai-client/src/analytics/selectors.ts @@ -1,2 +1,4 @@ -export const getHasOptedIn = (state: any): boolean | null => +import type { Mixpanel } from '../resources/types' + +export const getHasOptedIn = (state: Mixpanel): boolean | null => state.analytics.hasOptedIn diff --git a/opentrons-ai-client/src/assets/localization/en/create_protocol.json b/opentrons-ai-client/src/assets/localization/en/create_protocol.json new file mode 100644 index 00000000000..5bf2d5d6e23 --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/create_protocol.json @@ -0,0 +1,17 @@ +{ + "application_title": "Application", + "application_scientific_dropdown_title": "What's your scientific application?", + "application_scientific_dropdown_placeholder": "Select an option", + "basic_aliquoting": "Basic aliquoting", + "pcr": "PCR", + "other": "Other", + "application_other_title": "Other application", + "application_other_caption": "Example: “cherrypicking” or “serial dilution”", + "application_describe_title": "Describe what you are trying to do", + "application_describe_caption": "Example: “The protocol performs automated liquid handling for Pierce BCA Protein Assay Kit to determine protein concentrations in various sample types, such as cell lysates and eluates of purification process.", + "section_confirm_button": "Confirm", + "instruments_title": "Instruments", + "modules_title": "Modules", + "labware_liquids_title": "Labware & Liquids", + "steps_title": "Steps" +} diff --git a/opentrons-ai-client/src/assets/localization/en/index.ts b/opentrons-ai-client/src/assets/localization/en/index.ts index b5aa26621dd..078684c8548 100644 --- a/opentrons-ai-client/src/assets/localization/en/index.ts +++ b/opentrons-ai-client/src/assets/localization/en/index.ts @@ -1,7 +1,9 @@ import shared from './shared.json' import protocol_generator from './protocol_generator.json' +import create_protocol from './create_protocol.json' export const en = { shared, protocol_generator, + create_protocol, } diff --git a/opentrons-ai-client/src/atoms/ControlledDropdownMenu/index.tsx b/opentrons-ai-client/src/atoms/ControlledDropdownMenu/index.tsx new file mode 100644 index 00000000000..5f2463081a6 --- /dev/null +++ b/opentrons-ai-client/src/atoms/ControlledDropdownMenu/index.tsx @@ -0,0 +1,55 @@ +import { DropdownMenu } from '@opentrons/components' +import type { DropdownBorder, DropdownOption } from '@opentrons/components' +import { Controller } from 'react-hook-form' + +interface ControlledDropdownMenuProps { + id?: string + name: string + rules?: any + options: DropdownOption[] + width?: string + dropdownType?: DropdownBorder | undefined + title?: string + defaultOption?: string + placeholder?: string +} + +export function ControlledDropdownMenu({ + name, + rules, + options, + width, + dropdownType, + title, + defaultOption, + placeholder = '', +}: ControlledDropdownMenuProps): JSX.Element { + return ( + { + const fieldValueName = options.find( + option => option.value === field.value + )?.name + + return ( + { + field.onChange(e) + }} + /> + ) + }} + /> + ) +} diff --git a/opentrons-ai-client/src/atoms/ControlledInputField/index.tsx b/opentrons-ai-client/src/atoms/ControlledInputField/index.tsx new file mode 100644 index 00000000000..064fee48573 --- /dev/null +++ b/opentrons-ai-client/src/atoms/ControlledInputField/index.tsx @@ -0,0 +1,37 @@ +import { InputField } from '@opentrons/components' +import { Controller } from 'react-hook-form' + +interface ControlledInputFieldProps { + id?: string + name: string + rules?: any + title?: string + caption?: string +} + +export function ControlledInputField({ + id, + name, + rules, + title, + caption, +}: ControlledInputFieldProps): JSX.Element { + return ( + ( + + )} + /> + ) +} diff --git a/opentrons-ai-client/src/molecules/Accordion/Accordion.stories.tsx b/opentrons-ai-client/src/molecules/Accordion/Accordion.stories.tsx index 388267061b0..a96f5e56cf1 100644 --- a/opentrons-ai-client/src/molecules/Accordion/Accordion.stories.tsx +++ b/opentrons-ai-client/src/molecules/Accordion/Accordion.stories.tsx @@ -36,6 +36,7 @@ type Story = StoryObj export const AccordionCollapsed: Story = { args: { id: 'accordion', + isOpen: false, handleClick: () => { alert('Accordion clicked') }, diff --git a/opentrons-ai-client/src/molecules/Accordion/index.tsx b/opentrons-ai-client/src/molecules/Accordion/index.tsx index 885f6af1745..4bb611d618d 100644 --- a/opentrons-ai-client/src/molecules/Accordion/index.tsx +++ b/opentrons-ai-client/src/molecules/Accordion/index.tsx @@ -1,7 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef } from 'react' import styled from 'styled-components' import { - Flex, Icon, StyledText, COLORS, @@ -14,17 +13,16 @@ import { CURSOR_POINTER, TEXT_ALIGN_LEFT, DISPLAY_FLEX, - OVERFLOW_HIDDEN, CURSOR_DEFAULT, } from '@opentrons/components' interface AccordionProps { id?: string handleClick: () => void - heading: string isOpen?: boolean isCompleted?: boolean disabled?: boolean + heading?: string children: React.ReactNode } @@ -33,54 +31,6 @@ const BUTTON = 'button' const CONTENT = 'content' const OT_CHECK = 'ot-check' -const AccordionContainer = styled(Flex)<{ - isOpen: boolean - disabled: boolean -}>` - flex-direction: ${DIRECTION_COLUMN}; - width: 100%; - height: ${SIZE_AUTO}; - padding: ${SPACING.spacing24} ${SPACING.spacing32}; - border-radius: ${BORDERS.borderRadius16}; - background-color: ${COLORS.white}; - cursor: ${props => - props.isOpen || props.disabled ? `${CURSOR_DEFAULT}` : `${CURSOR_POINTER}`}; -` - -const AccordionButton = styled.button<{ isOpen: boolean; disabled: boolean }>` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_SPACE_BETWEEN}; - align-items: ${ALIGN_CENTER}; - width: 100%; - background: none; - border: none; - cursor: ${props => - props.isOpen || props.disabled ? `${CURSOR_DEFAULT}` : `${CURSOR_POINTER}`}; - text-align: ${TEXT_ALIGN_LEFT}; - - &:focus-visible { - outline: 2px solid ${COLORS.blue50}; - } -` - -const HeadingText = styled(StyledText)` - flex: 1; - margin-right: ${SPACING.spacing8}; -` - -const AccordionContent = styled.div<{ - id: string - isOpen: boolean - contentHeight: number -}>` - transition: height 0.3s ease, margin-top 0.3s ease, visibility 0.3s ease; - overflow: ${OVERFLOW_HIDDEN}; - height: ${props => (props.isOpen ? `${props.contentHeight}px` : '0')}; - margin-top: ${props => (props.isOpen ? `${SPACING.spacing16}` : '0')}; - pointer-events: ${props => (props.isOpen ? 'auto' : 'none')}; - visibility: ${props => (props.isOpen ? 'unset' : 'hidden')}; -` - export function Accordion({ id = ACCORDION, handleClick, @@ -91,16 +41,8 @@ export function Accordion({ children, }: AccordionProps): JSX.Element { const contentRef = useRef(null) - const [contentHeight, setContentHeight] = useState(0) - - useEffect(() => { - if (contentRef.current != null) { - setContentHeight(contentRef.current.scrollHeight) - } - }, [isOpen]) const handleContainerClick = (e: React.MouseEvent): void => { - // Prevent the click event from propagating to the button if ( (e.target as HTMLElement).tagName !== 'BUTTON' && !disabled && @@ -111,7 +53,6 @@ export function Accordion({ } const handleButtonClick = (e: React.MouseEvent): void => { - // Stop the event from propagating to the container if (!isOpen && !disabled) { e.stopPropagation() handleClick() @@ -148,7 +89,6 @@ export function Accordion({ role="region" aria-labelledby={`${id}-${BUTTON}`} isOpen={isOpen} - contentHeight={contentHeight} ref={contentRef} > {children} @@ -156,3 +96,49 @@ export function Accordion({ ) } + +const AccordionContainer = styled.div<{ + isOpen: boolean + disabled: boolean +}>` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + width: 100%; + height: ${SIZE_AUTO}; + padding: ${SPACING.spacing24} ${SPACING.spacing32}; + border-radius: ${BORDERS.borderRadius16}; + background-color: ${COLORS.white}; + cursor: ${props => + props.isOpen || props.disabled ? `${CURSOR_DEFAULT}` : `${CURSOR_POINTER}`}; +` + +const AccordionButton = styled.button<{ isOpen: boolean; disabled: boolean }>` + display: ${DISPLAY_FLEX}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + align-items: ${ALIGN_CENTER}; + width: 100%; + background: none; + border: none; + cursor: ${props => + props.isOpen || props.disabled ? `${CURSOR_DEFAULT}` : `${CURSOR_POINTER}`}; + text-align: ${TEXT_ALIGN_LEFT}; + + &:focus-visible { + outline: 2px solid ${COLORS.blue50}; + } +` + +const HeadingText = styled(StyledText)` + flex: 1; + margin-right: ${SPACING.spacing8}; +` + +const AccordionContent = styled.div<{ + id: string + isOpen: boolean +}>` + max-height: ${props => (props.isOpen ? `auto` : '0')}; + margin-top: ${props => (props.isOpen ? `${SPACING.spacing16}` : '0')}; + pointer-events: ${props => (props.isOpen ? 'auto' : 'none')}; + visibility: ${props => (props.isOpen ? 'visible' : 'hidden')}; +` diff --git a/opentrons-ai-client/src/molecules/PromptPreview/index.tsx b/opentrons-ai-client/src/molecules/PromptPreview/index.tsx index b789cfbb4c7..d74884ad1ae 100644 --- a/opentrons-ai-client/src/molecules/PromptPreview/index.tsx +++ b/opentrons-ai-client/src/molecules/PromptPreview/index.tsx @@ -30,6 +30,7 @@ const PromptPreviewContainer = styled(Flex)` ` const PromptPreviewHeading = styled(Flex)` + width: 100%; flex-direction: ${DIRECTION_ROW}; justify-content: ${JUSTIFY_SPACE_BETWEEN}; align-items: ${ALIGN_CENTER}; diff --git a/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx b/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx new file mode 100644 index 00000000000..aff027425e2 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx @@ -0,0 +1,81 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, it, expect } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { ApplicationSection } from '..' +import { FormProvider, useForm } from 'react-hook-form' + +const TestFormProviderComponent = () => { + const methods = useForm({ + defaultValues: {}, + }) + + return ( + + + + ) +} + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ApplicationSection', () => { + it('should render scientific application dropdown, describe input and confirm button', () => { + render() + + expect( + screen.getByText("What's your scientific application?") + ).toBeInTheDocument() + expect( + screen.getByText('Describe what you are trying to do') + ).toBeInTheDocument() + expect(screen.getByText('Confirm')).toBeInTheDocument() + }) + + it('should not render other application dropdown if Other option is not selected', () => { + render() + + expect(screen.queryByText('Other application')).not.toBeInTheDocument() + }) + + it('should render other application dropdown if Other option is selected', () => { + render() + + const applicationDropdown = screen.getByText('Select an option') + fireEvent.click(applicationDropdown) + + const otherOption = screen.getByText('Other') + fireEvent.click(otherOption) + + expect(screen.getByText('Other application')).toBeInTheDocument() + }) + + it('should enable confirm button when all fields are filled', async () => { + render() + + const applicationDropdown = screen.getByText('Select an option') + fireEvent.click(applicationDropdown) + + const basicAliquotingOption = screen.getByText('Basic aliquoting') + fireEvent.click(basicAliquotingOption) + + const describeInput = screen.getByRole('textbox') + fireEvent.change(describeInput, { target: { value: 'Test description' } }) + + const confirmButton = screen.getByRole('button') + await waitFor(() => { + expect(confirmButton).toBeEnabled() + }) + }) + + it('should disable confirm button when all fields are not filled', () => { + render() + + const confirmButton = screen.getByRole('button') + expect(confirmButton).toBeDisabled() + }) +}) diff --git a/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx b/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx new file mode 100644 index 00000000000..5e3cc523f68 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx @@ -0,0 +1,98 @@ +import { + DIRECTION_COLUMN, + DISPLAY_FLEX, + Flex, + JUSTIFY_FLEX_END, + LargeButton, + SPACING, +} from '@opentrons/components' +import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { ControlledDropdownMenu } from '../../atoms/ControlledDropdownMenu' +import { ControlledInputField } from '../../atoms/ControlledInputField' +import { useAtom } from 'jotai' +import { createProtocolAtom } from '../../resources/atoms' +import { APPLICATION_STEP } from '../ProtocolSectionsContainer' + +export const BASIC_ALIQUOTING = 'basic_aliquoting' +export const PCR = 'pcr' +export const OTHER = 'other' +export const APPLICATION_SCIENTIFIC_APPLICATION = + 'application.scientificApplication' +export const APPLICATION_OTHER_APPLICATION = 'application.otherApplication' +export const APPLICATION_DESCRIBE = 'application.description' + +export function ApplicationSection(): JSX.Element | null { + const { t } = useTranslation('create_protocol') + const { + watch, + formState: { isValid }, + } = useFormContext() + const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + + const options = [ + { name: t(BASIC_ALIQUOTING), value: BASIC_ALIQUOTING }, + { name: t(PCR), value: PCR }, + { name: t(OTHER), value: OTHER }, + ] + + const isOtherSelected = watch(APPLICATION_SCIENTIFIC_APPLICATION) === OTHER + + function handleConfirmButtonClick(): void { + const step = + currentStep > APPLICATION_STEP ? currentStep : APPLICATION_STEP + 1 + + setCreateProtocolAtom({ + currentStep: step, + focusStep: step, + }) + } + + return ( + + + + {isOtherSelected && ( + + )} + + + + + + + + ) +} + +const ButtonContainer = styled.div` + display: ${DISPLAY_FLEX}; + justify-content: ${JUSTIFY_FLEX_END}; +` diff --git a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/__tests__/ProtocolSectionsContainer.test.tsx b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/__tests__/ProtocolSectionsContainer.test.tsx new file mode 100644 index 00000000000..b438b2c9af6 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/__tests__/ProtocolSectionsContainer.test.tsx @@ -0,0 +1,101 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, it, expect } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { ProtocolSectionsContainer } from '..' +import { FormProvider, useForm } from 'react-hook-form' +import { fillApplicationSectionAndClickConfirm } from '../../../resources/utils/createProtocolTestUtils' + +const TestFormProviderComponent = () => { + const methods = useForm({ + defaultValues: {}, + }) + + return ( + + + + ) +} + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ProtocolSectionsContainer', () => { + it('should render all five accordions for each step of Protocol Creation', () => { + render() + + expect(screen.getByText('Application')).toBeInTheDocument() + expect(screen.getByText('Instruments')).toBeInTheDocument() + expect(screen.getByText('Modules')).toBeInTheDocument() + expect(screen.getByText('Labware & Liquids')).toBeInTheDocument() + expect(screen.getByText('Steps')).toBeInTheDocument() + }) + + it('should render the ApplicationSection opened by default', () => { + render() + + expect(screen.getByRole('button', { name: 'Application' })).toHaveAttribute( + 'aria-expanded', + 'true' + ) + }) + + it('should render all the other sections closed by default', () => { + render() + + expect(screen.getByRole('button', { name: 'Instruments' })).toHaveAttribute( + 'aria-expanded', + 'false' + ) + expect(screen.getByRole('button', { name: 'Modules' })).toHaveAttribute( + 'aria-expanded', + 'false' + ) + expect( + screen.getByRole('button', { name: 'Labware & Liquids' }) + ).toHaveAttribute('aria-expanded', 'false') + expect(screen.getByRole('button', { name: 'Steps' })).toHaveAttribute( + 'aria-expanded', + 'false' + ) + }) + + it('should go back to previous section when clicking on the previous section', async () => { + render() + + const applicationButton = screen.getByRole('button', { + name: 'Application', + }) + expect(applicationButton).toHaveAttribute('aria-expanded', 'true') + + await fillApplicationSectionAndClickConfirm() + + await waitFor(() => { + expect(applicationButton).toHaveAttribute('aria-expanded', 'false') + }) + fireEvent.click(applicationButton) + + await waitFor(() => { + expect(applicationButton).toHaveAttribute('aria-expanded', 'true') + }) + }) + + it('should not allow user to go to a future section', async () => { + render() + + const instrumentsButton = screen.getByRole('button', { + name: 'Instruments', + }) + expect(instrumentsButton).toHaveAttribute('aria-expanded', 'false') + + fireEvent.click(instrumentsButton) + + await waitFor(() => { + expect(instrumentsButton).toHaveAttribute('aria-expanded', 'false') + }) + }) +}) diff --git a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx new file mode 100644 index 00000000000..49314c1a143 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx @@ -0,0 +1,87 @@ +import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components' +import { useTranslation } from 'react-i18next' +import { Accordion } from '../../molecules/Accordion' +import styled from 'styled-components' +import { ApplicationSection } from '../../organisms/ApplicationSection' +import { createProtocolAtom } from '../../resources/atoms' +import { useAtom } from 'jotai' +import { useFormContext } from 'react-hook-form' + +export const APPLICATION_STEP = 0 +export const INSTRUMENTS_STEP = 1 +export const MODULES_STEP = 2 +export const LABWARE_LIQUIDS_STEP = 3 +export const STEPS_STEP = 4 + +export function ProtocolSectionsContainer(): JSX.Element | null { + const { t } = useTranslation('create_protocol') + const { + formState: { isValid }, + } = useFormContext() + const [{ currentStep, focusStep }, setCreateProtocolAtom] = useAtom( + createProtocolAtom + ) + + function handleSectionClick(stepNumber: number): void { + currentStep >= stepNumber && + isValid && + setCreateProtocolAtom({ + currentStep, + focusStep: stepNumber, + }) + } + + function displayCheckmark(stepNumber: number): boolean { + return currentStep > stepNumber && focusStep !== stepNumber + } + + return ( + + {[ + { + stepNumber: APPLICATION_STEP, + title: 'application_title', + Component: ApplicationSection, + }, + { + stepNumber: INSTRUMENTS_STEP, + title: 'instruments_title', + Component: () => Content, + }, + { + stepNumber: MODULES_STEP, + title: 'modules_title', + Component: () => Content, + }, + { + stepNumber: LABWARE_LIQUIDS_STEP, + title: 'labware_liquids_title', + Component: () => Content, + }, + { + stepNumber: STEPS_STEP, + title: 'steps_title', + Component: () => Content, + }, + ].map(({ stepNumber, title, Component }) => ( + { + handleSectionClick(stepNumber) + }} + isCompleted={displayCheckmark(stepNumber)} + > + + + ))} + + ) +} + +const ProtocolSections = styled(Flex)` + flex-direction: ${DIRECTION_COLUMN}; + width: 100%; + gap: ${SPACING.spacing16}; +` diff --git a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx new file mode 100644 index 00000000000..871bab07a7b --- /dev/null +++ b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx @@ -0,0 +1,84 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, it, expect } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { CreateProtocol } from '..' +import { Provider } from 'jotai' +import { fillApplicationSectionAndClickConfirm } from '../../../resources/utils/createProtocolTestUtils' + +const render = (): ReturnType => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + ) +} + +describe('CreateProtocol', () => { + it('should update the active section when user fills the section information and clicks the confirm button', async () => { + render() + + const buttonsAndAccordions = screen.getAllByRole('button') + expect(buttonsAndAccordions[0]).toHaveAttribute('aria-expanded', 'true') + + await fillApplicationSectionAndClickConfirm() + + await waitFor(() => { + expect(buttonsAndAccordions[0]).toHaveAttribute('aria-expanded', 'false') + }) + }) + + it('should display the Prompt preview correctly for Application section', async () => { + render() + + await fillApplicationSectionAndClickConfirm() + + const previewItems = screen.getAllByTestId('Tag_default') + + expect(previewItems).toHaveLength(2) + expect(previewItems[0]).toHaveTextContent('Basic aliquoting') + expect(previewItems[1]).toHaveTextContent('Test description') + }) + + it('should display the Prompt preview correctly for Application section if Other application is selected', () => { + render() + + const applicationDropdown = screen.getByText('Select an option') + fireEvent.click(applicationDropdown) + + const basicAliquotingOption = screen.getByText('Other') + fireEvent.click(basicAliquotingOption) + + const [otherInput, describeInput] = screen.getAllByRole('textbox') + + fireEvent.change(otherInput, { target: { value: 'Test Application' } }) + fireEvent.change(describeInput, { target: { value: 'Test description' } }) + + const confirmButton = screen.getByText('Confirm') + fireEvent.click(confirmButton) + + const promptPreview = screen.getByText('Prompt') + expect(promptPreview).toBeInTheDocument() + + const previewItems = screen.getAllByTestId('Tag_default') + expect(previewItems).toHaveLength(2) + expect(previewItems[0]).toHaveTextContent('Test Application') + expect(previewItems[1]).toHaveTextContent('Test description') + }) + + it('should display a completed checkmark if the section is completed', async () => { + render() + + expect(screen.queryByTestId('accordion-ot-check')).not.toBeInTheDocument() + + const buttonsAndAccordions = screen.getAllByRole('button') + expect(buttonsAndAccordions[0]).toHaveAttribute('aria-expanded', 'true') + + await fillApplicationSectionAndClickConfirm() + + expect(screen.getByTestId('accordion-ot-check')).toBeInTheDocument() + }) +}) diff --git a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx new file mode 100644 index 00000000000..346e43c879a --- /dev/null +++ b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx @@ -0,0 +1,101 @@ +import { + Flex, + JUSTIFY_SPACE_EVENLY, + POSITION_RELATIVE, + SPACING, +} from '@opentrons/components' +import { useTranslation } from 'react-i18next' +import { useEffect } from 'react' +import { PromptPreview } from '../../molecules/PromptPreview' +import { useForm, FormProvider } from 'react-hook-form' +import { createProtocolAtom, headerWithMeterAtom } from '../../resources/atoms' +import { useAtom } from 'jotai' +import { ProtocolSectionsContainer } from '../../organisms/ProtocolSectionsContainer' +import { OTHER } from '../../organisms/ApplicationSection' + +interface CreateProtocolFormData { + application: { + scientificApplication: string + otherApplication?: string + description: string + } +} + +const TOTAL_STEPS = 5 + +export function CreateProtocol(): JSX.Element | null { + const { t } = useTranslation('create_protocol') + const [, setHeaderWithMeterAtom] = useAtom(headerWithMeterAtom) + const [{ currentStep }] = useAtom(createProtocolAtom) + + const methods = useForm({ + defaultValues: { + application: { + scientificApplication: '', + otherApplication: '', + description: '', + }, + }, + }) + + function calculateProgress(): number { + return currentStep > 0 ? currentStep / TOTAL_STEPS : 0 + } + + useEffect(() => { + setHeaderWithMeterAtom({ + displayHeaderWithMeter: true, + progress: calculateProgress(), + }) + }, [currentStep]) + + function generatePromptPreviewApplicationItems(): string[] { + const { + application: { scientificApplication, otherApplication, description }, + } = methods.watch() + + const scientificOrOtherApplication = + scientificApplication === OTHER + ? otherApplication + : scientificApplication !== '' + ? t(scientificApplication) + : '' + + return [ + scientificOrOtherApplication !== '' && scientificOrOtherApplication, + description !== '' && description, + ].filter(Boolean) + } + + function generatePromptPreviewData(): Array<{ + title: string + items: string[] + }> { + return [ + { + title: t('application_title'), + items: generatePromptPreviewApplicationItems(), + }, + ] + } + + return ( + + + + + + + ) +} diff --git a/opentrons-ai-client/src/pages/Landing/index.tsx b/opentrons-ai-client/src/pages/Landing/index.tsx index b464ad5ff29..cda92f7052b 100644 --- a/opentrons-ai-client/src/pages/Landing/index.tsx +++ b/opentrons-ai-client/src/pages/Landing/index.tsx @@ -17,10 +17,6 @@ import { useIsMobile } from '../../resources/hooks/useIsMobile' import { useNavigate } from 'react-router-dom' import { useTrackEvent } from '../../resources/hooks/useTrackEvent' -export interface InputType { - userPrompt: string -} - export function Landing(): JSX.Element | null { const navigate = useNavigate() const { t } = useTranslation('protocol_generator') diff --git a/opentrons-ai-client/src/resources/atoms.ts b/opentrons-ai-client/src/resources/atoms.ts index 73d45fb165b..3ea530c65f6 100644 --- a/opentrons-ai-client/src/resources/atoms.ts +++ b/opentrons-ai-client/src/resources/atoms.ts @@ -1,6 +1,12 @@ // jotai's atoms import { atom } from 'jotai' -import type { Chat, ChatData, Mixpanel } from './types' +import type { + Chat, + ChatData, + createProtocolAtomProps, + HeaderWithMeterAtomProps, + Mixpanel, +} from './types' /** ChatDataAtom is for chat data (user prompt and response from OpenAI API) */ export const chatDataAtom = atom([]) @@ -10,5 +16,16 @@ export const chatHistoryAtom = atom([]) export const tokenAtom = atom(null) export const mixpanelAtom = atom({ - analytics: { hasOptedIn: true }, // TODO: set to false + analytics: { hasOptedIn: true }, // TODO: set to false when we have the opt-in modal + isInitialized: false, +}) + +export const headerWithMeterAtom = atom({ + displayHeaderWithMeter: false, + progress: 0, +}) + +export const createProtocolAtom = atom({ + currentStep: 0, + focusStep: 0, }) diff --git a/opentrons-ai-client/src/resources/hooks/__tests__/useTrackEvent.test.tsx b/opentrons-ai-client/src/resources/hooks/__tests__/useTrackEvent.test.tsx index fab96155156..0c43e0bca24 100644 --- a/opentrons-ai-client/src/resources/hooks/__tests__/useTrackEvent.test.tsx +++ b/opentrons-ai-client/src/resources/hooks/__tests__/useTrackEvent.test.tsx @@ -5,24 +5,33 @@ import { renderHook } from '@testing-library/react' import { mixpanelAtom } from '../../atoms' import type { AnalyticsEvent } from '../../../analytics/mixpanel' import type { Mixpanel } from '../../types' -import { TestProvider } from '../../utils/testUtils' +import { TestProvider } from '../../../__testing-utils__' vi.mock('../../../analytics/mixpanel', () => ({ trackEvent: vi.fn(), })) +const mockMixpanelAtom: Mixpanel = { + analytics: { + hasOptedIn: true, + }, + isInitialized: false, +} + +const wrapper = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ) +} + describe('useTrackEvent', () => { afterEach(() => { vi.resetAllMocks() }) it('should call trackEvent with the correct arguments when hasOptedIn is true', () => { - const mockMixpanelAtom: Mixpanel = { - analytics: { - hasOptedIn: true, - }, - } - const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -38,17 +47,7 @@ describe('useTrackEvent', () => { }) it('should call trackEvent with the correct arguments when hasOptedIn is false', () => { - const mockMixpanelAtomFalse: Mixpanel = { - analytics: { - hasOptedIn: false, - }, - } - - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ) + mockMixpanelAtom.analytics.hasOptedIn = false const { result } = renderHook(() => useTrackEvent(), { wrapper }) diff --git a/opentrons-ai-client/src/resources/types.ts b/opentrons-ai-client/src/resources/types.ts index 067c1ef9764..410bdfd98a6 100644 --- a/opentrons-ai-client/src/resources/types.ts +++ b/opentrons-ai-client/src/resources/types.ts @@ -35,6 +35,7 @@ export interface Mixpanel { analytics: { hasOptedIn: boolean } + isInitialized: boolean } export interface AnalyticsEvent { @@ -42,3 +43,29 @@ export interface AnalyticsEvent { properties: Record superProperties?: Record } + +export interface HeaderWithMeterAtomProps { + displayHeaderWithMeter: boolean + progress: number +} + +export interface createProtocolAtomProps { + currentStep: number + focusStep: number +} + +export interface PromptData { + /** assistant: ChatGPT API, user: user */ + role: Role + /** content gathered from the user selection */ + data: { + applicationSection: { + application: string + description: string + } + instrumentsSection: { + robot: string + instruments: string[] + } + } +} diff --git a/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx new file mode 100644 index 00000000000..8a24224394e --- /dev/null +++ b/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx @@ -0,0 +1,19 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { expect } from 'vitest' + +export async function fillApplicationSectionAndClickConfirm(): Promise { + const applicationDropdown = screen.getByText('Select an option') + fireEvent.click(applicationDropdown) + + const basicAliquotingOption = screen.getByText('Basic aliquoting') + fireEvent.click(basicAliquotingOption) + + const describeInput = screen.getByRole('textbox') + fireEvent.change(describeInput, { target: { value: 'Test description' } }) + + const confirmButton = screen.getByText('Confirm') + await waitFor(() => { + expect(confirmButton).toBeEnabled() + }) + fireEvent.click(confirmButton) +} diff --git a/opentrons-ai-client/src/resources/utils/testUtils.tsx b/opentrons-ai-client/src/resources/utils/testUtils.tsx deleted file mode 100644 index 954307bd391..00000000000 --- a/opentrons-ai-client/src/resources/utils/testUtils.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Provider } from 'jotai' -import { useHydrateAtoms } from 'jotai/utils' - -interface HydrateAtomsProps { - initialValues: Array<[any, any]> - children: React.ReactNode -} - -interface TestProviderProps { - initialValues: Array<[any, any]> - children: React.ReactNode -} - -export const HydrateAtoms = ({ - initialValues, - children, -}: HydrateAtomsProps): React.ReactNode => { - useHydrateAtoms(initialValues) - return children -} - -export const TestProvider = ({ - initialValues, - children, -}: TestProviderProps): React.ReactNode => ( - - {children} - -) diff --git a/protocol-designer/package.json b/protocol-designer/package.json index 045082d63be..547b15de023 100755 --- a/protocol-designer/package.json +++ b/protocol-designer/package.json @@ -27,7 +27,6 @@ "@opentrons/components": "link:../components", "@opentrons/step-generation": "link:../step-generation", "@opentrons/shared-data": "link:../shared-data", - "@types/react-lottie": "^1.2.10", "@types/redux-actions": "2.6.1", "@types/styled-components": "^5.1.26", "@types/ua-parser-js": "0.7.36", @@ -50,9 +49,9 @@ "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", + "react-error-boundary": "^4.0.10", "react-hook-form": "7.49.3", "react-i18next": "14.0.0", - "react-lottie": "^1.2.4", "react-redux": "8.1.2", "redux": "4.0.5", "redux-actions": "2.2.1", diff --git a/protocol-designer/src/ProtocolEditor.tsx b/protocol-designer/src/ProtocolEditor.tsx index 8c9fb9fe1a0..9bd7ef9cb48 100644 --- a/protocol-designer/src/ProtocolEditor.tsx +++ b/protocol-designer/src/ProtocolEditor.tsx @@ -20,6 +20,8 @@ import { GateModal } from './organisms/GateModal' import { CreateFileWizard } from './components/modals/CreateFileWizard' import { AnnouncementModal } from './organisms' import { ProtocolRoutes } from './ProtocolRoutes' +import { useScreenSizeCheck } from './resources/useScreenSizeCheck' +import { DisabledScreen } from './organisms/DisabledScreen' import styles from './components/ProtocolEditor.module.css' import './css/reset.module.css' @@ -29,7 +31,7 @@ const showGateModal = function ProtocolEditorComponent(): JSX.Element { const enableRedesign = useSelector(getEnableRedesign) - + const isValidSize = useScreenSizeCheck() return (
{enableRedesign ? ( + {!isValidSize && } diff --git a/protocol-designer/src/ProtocolRoutes.tsx b/protocol-designer/src/ProtocolRoutes.tsx index 908f46539be..b491ac8ca60 100644 --- a/protocol-designer/src/ProtocolRoutes.tsx +++ b/protocol-designer/src/ProtocolRoutes.tsx @@ -1,4 +1,5 @@ -import { Route, Navigate, Routes } from 'react-router-dom' +import { Route, Navigate, Routes, useNavigate } from 'react-router-dom' +import { ErrorBoundary } from 'react-error-boundary' import { Box } from '@opentrons/components' import { Landing } from './pages/Landing' import { ProtocolOverview } from './pages/ProtocolOverview' @@ -13,6 +14,7 @@ import { LabwareUploadModal, GateModal, } from './organisms' +import { ProtocolDesignerAppFallback } from './resources/ProtocolDesignerAppFallback' import type { RouteProps } from './types' @@ -60,9 +62,18 @@ export function ProtocolRoutes(): JSX.Element { const showGateModal = process.env.NODE_ENV === 'production' || process.env.OT_PD_SHOW_GATE + const navigate = useNavigate() + const handleReset = (): void => { + navigate('/', { replace: true }) + } + return ( - <> + + {showGateModal ? : null} @@ -76,6 +87,6 @@ export function ProtocolRoutes(): JSX.Element { - + ) } diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index 8151c1e3270..b98f29e6beb 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -23,6 +23,8 @@ "edit_protocol_metadata": "Edit protocol metadata", "edit": "edit", "eight_channel": "8-Channel", + "error_boundary_pd_app_description": "You need to reload the app. Contact support with the following error message:", + "error_boundary_title": "An unknown error has occurred", "exact_labware_match": "Duplicate labware definition", "exit": "exit", "fixtures": "Fixtures", @@ -105,8 +107,10 @@ "re_export": "To use this definition, use Labware Creator to give it a unique load name and display name.", "remove": "remove", "reject": "Reject", + "reload_app": "Reload app", "reset_hints_and_tips": "Reset all hints and tips notifications", "reset_hints": "Reset hints", + "resize_your_browser": "Resize your browser to at least 600px wide and 650px tall to continue editing your protocol", "review_our_privacy_policy": "You can adjust this setting at any time by clicking on the settings icon. Find detailed information in our privacy policy.", "right": "Right", "save": "Save", @@ -135,5 +139,6 @@ "we_are_improving": "In order to improve our products, Opentrons would like to collect data related to your use of Protocol Designer. With your consent, Opentrons will collect and store analytics and session data, including through the use of cookies and similar technologies, solely for the purpose enhancing our products. Find detailed information in our privacy policy. By using Protocol Designer, you consent to the Opentrons EULA.", "welcome": "Welcome to Protocol Designer!", "opentrons_collects_data": "In order to improve our products, Opentrons would like to collect data related to your use of Protocol Designer. With your consent, Opentrons will collect and store analytics and session data, including through the use of cookies and similar technologies, solely for the purpose enhancing our products.", - "yes": "Yes" + "yes": "Yes", + "your_screen_is_too_small": "Your browser size is too small" } diff --git a/protocol-designer/src/organisms/DisabledScreen/__tests__/DisabledScreen.test.tsx b/protocol-designer/src/organisms/DisabledScreen/__tests__/DisabledScreen.test.tsx new file mode 100644 index 00000000000..abb537bae4a --- /dev/null +++ b/protocol-designer/src/organisms/DisabledScreen/__tests__/DisabledScreen.test.tsx @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { COLORS } from '@opentrons/components' + +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { DisabledScreen } from '..' + +const render = () => { + return renderWithProviders(, { i18nInstance: i18n }) +} + +describe('DisabledScreen', () => { + it('should render icon and text', () => { + render() + screen.getByTestId('browser_icon_in_DisabledScreen') + screen.getByText('Your browser size is too small') + screen.getByText( + 'Resize your browser to at least 600px wide and 650px tall to continue editing your protocol' + ) + }) + + it('should render background with transparent', () => { + render() + expect(screen.getByLabelText('BackgroundOverlay_ModalShell')).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity40HexCode}` + ) + }) + + it('should render white text', () => { + render() + expect(screen.getByText('Your browser size is too small')).toHaveStyle( + `color: ${COLORS.white}` + ) + expect( + screen.getByText( + 'Resize your browser to at least 600px wide and 650px tall to continue editing your protocol' + ) + ).toHaveStyle(`color: ${COLORS.white}`) + }) +}) diff --git a/protocol-designer/src/organisms/DisabledScreen/index.tsx b/protocol-designer/src/organisms/DisabledScreen/index.tsx new file mode 100644 index 00000000000..bef9f0cd643 --- /dev/null +++ b/protocol-designer/src/organisms/DisabledScreen/index.tsx @@ -0,0 +1,60 @@ +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_CENTER, + ModalShell, + OVERFLOW_HIDDEN, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { getTopPortalEl } from '../../components/portals/TopPortal' + +export function DisabledScreen(): JSX.Element { + const { t } = useTranslation('shared') + + return createPortal( + + + + + + {t('your_screen_is_too_small')} + + + {t('resize_your_browser')} + + + + , + getTopPortalEl() + ) +} diff --git a/protocol-designer/src/resources/ProtocolDesignerAppFallback.tsx b/protocol-designer/src/resources/ProtocolDesignerAppFallback.tsx new file mode 100644 index 00000000000..32eabb78a30 --- /dev/null +++ b/protocol-designer/src/resources/ProtocolDesignerAppFallback.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from 'react-i18next' + +// ToDo need to add analytics + +import type { FallbackProps } from 'react-error-boundary' + +import { + AlertPrimaryButton, + ALIGN_FLEX_END, + DIRECTION_COLUMN, + Flex, + Modal, + SPACING, + StyledText, +} from '@opentrons/components' + +export function ProtocolDesignerAppFallback({ + error, + resetErrorBoundary, +}: FallbackProps): JSX.Element { + const { t } = useTranslation('shared') + + const handleReloadClick = (): void => { + resetErrorBoundary() + } + + return ( + + + + + {t('error_boundary_pd_app_description')} + + + {error.message} + + + + {t('reload_app')} + + + + ) +} diff --git a/protocol-designer/src/resources/__tests__/ProtocolDesignerAppFallback.test.tsx b/protocol-designer/src/resources/__tests__/ProtocolDesignerAppFallback.test.tsx new file mode 100644 index 00000000000..20c32526e63 --- /dev/null +++ b/protocol-designer/src/resources/__tests__/ProtocolDesignerAppFallback.test.tsx @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' + +import { i18n } from '../../assets/localization' +import { renderWithProviders } from '../../__testing-utils__' +import { ProtocolDesignerAppFallback } from '../ProtocolDesignerAppFallback' + +import type { FallbackProps } from 'react-error-boundary' + +const mockError = { + message: 'mock error', +} as Error + +const mockFunc = vi.fn() + +const render = (props: FallbackProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ProtocolDesignerAppFallback', () => { + let props: FallbackProps + + beforeEach(() => { + props = { + error: mockError, + resetErrorBoundary: mockFunc, + } as FallbackProps + }) + + it('should render text and button', () => { + render(props) + screen.getByText('An unknown error has occurred') + screen.getByText( + 'You need to reload the app. Contact support with the following error message:' + ) + screen.getByText('Reload app') + }) + + it('should call mock function when clicking the button', () => { + render(props) + fireEvent.click(screen.getByText('Reload app')) + expect(mockFunc).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/resources/__tests__/useScreenSizeCheck.test.ts b/protocol-designer/src/resources/__tests__/useScreenSizeCheck.test.ts new file mode 100644 index 00000000000..a897447b368 --- /dev/null +++ b/protocol-designer/src/resources/__tests__/useScreenSizeCheck.test.ts @@ -0,0 +1,28 @@ +import { describe, it, vi, expect } from 'vitest' +import { renderHook } from '@testing-library/react' +import { useScreenSizeCheck } from '../useScreenSizeCheck' + +describe('useScreenSizeCheck', () => { + it('should return true if the window size is greater than 600px x 650px', () => { + vi.stubGlobal('innerWidth', 1440) + vi.stubGlobal('innerHeight', 900) + const { result } = renderHook(() => useScreenSizeCheck()) + expect(result.current).toBe(true) + }) + + it('should return false if the window height is less than 650px', () => { + vi.stubGlobal('innerWidth', 1440) + vi.stubGlobal('innerHeight', 649) + window.dispatchEvent(new Event('resize')) + const { result } = renderHook(() => useScreenSizeCheck()) + expect(result.current).toBe(false) + }) + + it('should return false if the window width is less than 600px', () => { + vi.stubGlobal('innerWidth', 599) + vi.stubGlobal('innerHeight', 900) + window.dispatchEvent(new Event('resize')) + const { result } = renderHook(() => useScreenSizeCheck()) + expect(result.current).toBe(false) + }) +}) diff --git a/protocol-designer/src/resources/useScreenSizeCheck.ts b/protocol-designer/src/resources/useScreenSizeCheck.ts new file mode 100644 index 00000000000..ab95f3fe0e1 --- /dev/null +++ b/protocol-designer/src/resources/useScreenSizeCheck.ts @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react' + +const BREAKPOINT_HEIGHT = 650 +const BREAKPOINT_WIDTH = 600 + +export const useScreenSizeCheck = (): boolean => { + const [isValidSize, setValidSize] = useState( + window.innerWidth > BREAKPOINT_WIDTH && + window.innerHeight > BREAKPOINT_HEIGHT + ) + + useEffect(() => { + const handleResize = (): void => { + setValidSize( + window.innerWidth > BREAKPOINT_WIDTH && + window.innerHeight > BREAKPOINT_HEIGHT + ) + } + + window.addEventListener('resize', handleResize) + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return isValidSize +} diff --git a/react-api-client/src/runs/__tests__/useResumeFromRecoveryAssumingFalsePositiveMutation.test.tsx b/react-api-client/src/runs/__tests__/useResumeFromRecoveryAssumingFalsePositiveMutation.test.tsx new file mode 100644 index 00000000000..ec5900d4a62 --- /dev/null +++ b/react-api-client/src/runs/__tests__/useResumeFromRecoveryAssumingFalsePositiveMutation.test.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { act, renderHook, waitFor } from '@testing-library/react' + +import { createRunAction } from '@opentrons/api-client' + +import { useHost } from '../../api' +import { useResumeRunFromRecoveryAssumingFalsePositiveMutation } from '..' + +import { RUN_ID_1, mockResumeFromRecoveryAction } from '../__fixtures__' + +import type * as React from 'react' +import type { HostConfig, Response, RunAction } from '@opentrons/api-client' +import type { UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions } from '../useResumeFromRecoveryAssumingFalsePositiveMutation' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } + +describe('useResumeRunFromRecoveryAssumingFalsePositiveMutation hook', () => { + let wrapper: React.FunctionComponent< + { + children: React.ReactNode + } & UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions + > + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent< + { + children: React.ReactNode + } & UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions + > = ({ children }) => ( + {children} + ) + wrapper = clientProvider + }) + + it('should return no data when calling resumeRunFromRecoveryAssumingFalsePositive if the request fails', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createRunAction).mockRejectedValue('oh no') + + const { result } = renderHook( + useResumeRunFromRecoveryAssumingFalsePositiveMutation, + { + wrapper, + } + ) + + expect(result.current.data).toBeUndefined() + act(() => + result.current.resumeRunFromRecoveryAssumingFalsePositive(RUN_ID_1) + ) + await waitFor(() => { + expect(result.current.data).toBeUndefined() + }) + }) + + it('should create a resumeFromRecoveryAssumingFalsePositive run action when calling the resumeRunFromRecoveryAssumingFalsePositive callback', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createRunAction).mockResolvedValue({ + data: mockResumeFromRecoveryAction, + } as Response) + + const { result } = renderHook( + useResumeRunFromRecoveryAssumingFalsePositiveMutation, + { + wrapper, + } + ) + act(() => + result.current.resumeRunFromRecoveryAssumingFalsePositive(RUN_ID_1) + ) + + await waitFor(() => { + expect(result.current.data).toEqual(mockResumeFromRecoveryAction) + }) + }) +}) diff --git a/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx b/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx index cf9794c0dd8..f556b105153 100644 --- a/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx +++ b/react-api-client/src/runs/__tests__/useResumeRunFromRecoveryMutation.test.tsx @@ -9,22 +9,22 @@ import { useResumeRunFromRecoveryMutation } from '..' import { RUN_ID_1, mockResumeFromRecoveryAction } from '../__fixtures__' import type { HostConfig, Response, RunAction } from '@opentrons/api-client' -import type { UsePlayRunMutationOptions } from '../usePlayRunMutation' +import type { UseResumeRunFromRecoveryMutationOptions } from '../useResumeRunFromRecoveryMutation' vi.mock('@opentrons/api-client') vi.mock('../../api/useHost') const HOST_CONFIG: HostConfig = { hostname: 'localhost' } -describe('usePlayRunMutation hook', () => { +describe('useResumeRunFromRecoveryMutation hook', () => { let wrapper: React.FunctionComponent< - { children: React.ReactNode } & UsePlayRunMutationOptions + { children: React.ReactNode } & UseResumeRunFromRecoveryMutationOptions > beforeEach(() => { const queryClient = new QueryClient() const clientProvider: React.FunctionComponent< - { children: React.ReactNode } & UsePlayRunMutationOptions + { children: React.ReactNode } & UseResumeRunFromRecoveryMutationOptions > = ({ children }) => ( {children} ) diff --git a/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx b/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx index 7b40e4fc88e..e5c5c4cf265 100644 --- a/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx +++ b/react-api-client/src/runs/__tests__/useRunActionMutations.test.tsx @@ -10,18 +10,21 @@ import { usePauseRunMutation, useStopRunMutation, useResumeRunFromRecoveryMutation, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '..' import type { UsePlayRunMutationResult, UsePauseRunMutationResult, UseStopRunMutationResult, UseResumeRunFromRecoveryMutationResult, + UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult, } from '..' vi.mock('../usePlayRunMutation') vi.mock('../usePauseRunMutation') vi.mock('../useStopRunMutation') vi.mock('../useResumeRunFromRecoveryMutation') +vi.mock('../useResumeFromRecoveryAssumingFalsePositiveMutation') describe('useRunActionMutations hook', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> @@ -44,6 +47,7 @@ describe('useRunActionMutations hook', () => { const mockPauseRun = vi.fn() const mockStopRun = vi.fn() const mockResumeRunFromRecovery = vi.fn() + const mockResumeRunFromRecoveryAssumingFalsePositive = vi.fn() vi.mocked(usePlayRunMutation).mockReturnValue(({ playRun: mockPlayRun, @@ -61,6 +65,12 @@ describe('useRunActionMutations hook', () => { resumeRunFromRecovery: mockResumeRunFromRecovery, } as unknown) as UseResumeRunFromRecoveryMutationResult) + vi.mocked( + useResumeRunFromRecoveryAssumingFalsePositiveMutation + ).mockReturnValue(({ + resumeRunFromRecoveryAssumingFalsePositive: mockResumeRunFromRecoveryAssumingFalsePositive, + } as unknown) as UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult) + const { result } = renderHook(() => useRunActionMutations(RUN_ID_1), { wrapper, }) @@ -77,5 +87,12 @@ describe('useRunActionMutations hook', () => { act(() => result.current.resumeRunFromRecovery()) expect(mockResumeRunFromRecovery).toHaveBeenCalledTimes(1) expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(RUN_ID_1) + act(() => result.current.resumeRunFromRecoveryAssumingFalsePositive()) + expect( + mockResumeRunFromRecoveryAssumingFalsePositive + ).toHaveBeenCalledTimes(1) + expect(mockResumeRunFromRecoveryAssumingFalsePositive).toHaveBeenCalledWith( + RUN_ID_1 + ) }) }) diff --git a/react-api-client/src/runs/index.ts b/react-api-client/src/runs/index.ts index 71e3360a5f9..5e479ed5093 100644 --- a/react-api-client/src/runs/index.ts +++ b/react-api-client/src/runs/index.ts @@ -10,6 +10,7 @@ export { usePlayRunMutation } from './usePlayRunMutation' export { usePauseRunMutation } from './usePauseRunMutation' export { useStopRunMutation } from './useStopRunMutation' export { useResumeRunFromRecoveryMutation } from './useResumeRunFromRecoveryMutation' +export { useResumeRunFromRecoveryAssumingFalsePositiveMutation } from './useResumeFromRecoveryAssumingFalsePositiveMutation' export { useRunActionMutations } from './useRunActionMutations' export { useAllCommandsQuery } from './useAllCommandsQuery' export { useAllCommandsAsPreSerializedList } from './useAllCommandsAsPreSerializedList' @@ -23,3 +24,4 @@ export type { UsePlayRunMutationResult } from './usePlayRunMutation' export type { UsePauseRunMutationResult } from './usePauseRunMutation' export type { UseStopRunMutationResult } from './useStopRunMutation' export type { UseResumeRunFromRecoveryMutationResult } from './useResumeRunFromRecoveryMutation' +export type { UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult } from './useResumeFromRecoveryAssumingFalsePositiveMutation' diff --git a/react-api-client/src/runs/useResumeFromRecoveryAssumingFalsePositiveMutation.ts b/react-api-client/src/runs/useResumeFromRecoveryAssumingFalsePositiveMutation.ts new file mode 100644 index 00000000000..6eb10990053 --- /dev/null +++ b/react-api-client/src/runs/useResumeFromRecoveryAssumingFalsePositiveMutation.ts @@ -0,0 +1,60 @@ +import { useMutation } from 'react-query' + +import { + RUN_ACTION_TYPE_RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE, + createRunAction, +} from '@opentrons/api-client' + +import { useHost } from '../api' + +import type { AxiosError } from 'axios' +import type { + UseMutateFunction, + UseMutationOptions, + UseMutationResult, +} from 'react-query' +import type { HostConfig, RunAction } from '@opentrons/api-client' + +export type UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult = UseMutationResult< + RunAction, + AxiosError, + string +> & { + resumeRunFromRecoveryAssumingFalsePositive: UseMutateFunction< + RunAction, + AxiosError, + string + > +} + +export type UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions = UseMutationOptions< + RunAction, + AxiosError, + string +> + +export const useResumeRunFromRecoveryAssumingFalsePositiveMutation = ( + options: UseResumeRunFromRecoveryAssumingFalsePositiveMutationOptions = {} +): UseResumeRunFromRecoveryAssumingFalsePositiveMutationResult => { + const host = useHost() + const mutation = useMutation( + [ + host, + 'runs', + RUN_ACTION_TYPE_RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE, + ], + (runId: string) => + createRunAction(host as HostConfig, runId, { + actionType: RUN_ACTION_TYPE_RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE, + }) + .then(response => response.data) + .catch(e => { + throw e + }), + options + ) + return { + ...mutation, + resumeRunFromRecoveryAssumingFalsePositive: mutation.mutate, + } +} diff --git a/react-api-client/src/runs/useRunActionMutations.ts b/react-api-client/src/runs/useRunActionMutations.ts index 8bf3a08f1cb..a64411e7209 100644 --- a/react-api-client/src/runs/useRunActionMutations.ts +++ b/react-api-client/src/runs/useRunActionMutations.ts @@ -5,6 +5,7 @@ import { usePauseRunMutation, useStopRunMutation, useResumeRunFromRecoveryMutation, + useResumeRunFromRecoveryAssumingFalsePositiveMutation, } from '..' interface UseRunActionMutations { @@ -12,10 +13,12 @@ interface UseRunActionMutations { pauseRun: () => void stopRun: () => void resumeRunFromRecovery: () => void + resumeRunFromRecoveryAssumingFalsePositive: () => void isPlayRunActionLoading: boolean isPauseRunActionLoading: boolean isStopRunActionLoading: boolean isResumeRunFromRecoveryActionLoading: boolean + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading: boolean } export function useRunActionMutations(runId: string): UseRunActionMutations { @@ -43,6 +46,11 @@ export function useRunActionMutations(runId: string): UseRunActionMutations { isLoading: isResumeRunFromRecoveryActionLoading, } = useResumeRunFromRecoveryMutation() + const { + resumeRunFromRecoveryAssumingFalsePositive, + isLoading: isResumeRunFromRecoveryAssumingFalsePositiveActionLoading, + } = useResumeRunFromRecoveryAssumingFalsePositiveMutation() + return { playRun: () => { playRun(runId) @@ -56,9 +64,13 @@ export function useRunActionMutations(runId: string): UseRunActionMutations { resumeRunFromRecovery: () => { resumeRunFromRecovery(runId) }, + resumeRunFromRecoveryAssumingFalsePositive: () => { + resumeRunFromRecoveryAssumingFalsePositive(runId) + }, isPlayRunActionLoading, isPauseRunActionLoading, isStopRunActionLoading, isResumeRunFromRecoveryActionLoading, + isResumeRunFromRecoveryAssumingFalsePositiveActionLoading, } } diff --git a/robot-server/robot_server/runs/error_recovery_mapping.py b/robot-server/robot_server/runs/error_recovery_mapping.py index b548394cd8a..52da8caaad8 100644 --- a/robot-server/robot_server/runs/error_recovery_mapping.py +++ b/robot-server/robot_server/runs/error_recovery_mapping.py @@ -101,11 +101,9 @@ def _rule_matches_error( def _map_error_recovery_type(reaction_if_match: ReactionIfMatch) -> ErrorRecoveryType: match reaction_if_match: case ReactionIfMatch.IGNORE_AND_CONTINUE: - return ErrorRecoveryType.IGNORE_AND_CONTINUE + return ErrorRecoveryType.CONTINUE_WITH_ERROR case ReactionIfMatch.ASSUME_FALSE_POSITIVE_AND_CONTINUE: - # todo(mm, 2024-10-23): Connect to work in - # https://github.com/Opentrons/opentrons/pull/16556. - return ErrorRecoveryType.IGNORE_AND_CONTINUE + return ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE case ReactionIfMatch.FAIL_RUN: return ErrorRecoveryType.FAIL_RUN case ReactionIfMatch.WAIT_FOR_RECOVERY: diff --git a/robot-server/robot_server/runs/run_controller.py b/robot-server/robot_server/runs/run_controller.py index 1619cd20a08..41252d4dfc3 100644 --- a/robot-server/robot_server/runs/run_controller.py +++ b/robot-server/robot_server/runs/run_controller.py @@ -95,15 +95,22 @@ def create_action( self._task_runner.run(self._run_orchestrator_store.stop) elif action_type == RunActionType.RESUME_FROM_RECOVERY: - self._run_orchestrator_store.resume_from_recovery() + log.info(f'Resuming run "{self._run_id}" from error recovery mode.') + self._run_orchestrator_store.resume_from_recovery( + reconcile_false_positive=False + ) elif ( action_type == RunActionType.RESUME_FROM_RECOVERY_ASSUMING_FALSE_POSITIVE ): - # todo(mm, 2024-10-23): Connect to work in - # https://github.com/Opentrons/opentrons/pull/16556. - self._run_orchestrator_store.resume_from_recovery() + log.info( + f'Resuming run "{self._run_id}" from error recovery mode,' + f" assuming false-positive." + ) + self._run_orchestrator_store.resume_from_recovery( + reconcile_false_positive=True + ) else: assert_never(action_type) diff --git a/robot-server/robot_server/runs/run_orchestrator_store.py b/robot-server/robot_server/runs/run_orchestrator_store.py index efa97347ae9..bbc070c3b6f 100644 --- a/robot-server/robot_server/runs/run_orchestrator_store.py +++ b/robot-server/robot_server/runs/run_orchestrator_store.py @@ -310,9 +310,9 @@ async def stop(self) -> None: """Stop the run.""" await self.run_orchestrator.stop() - def resume_from_recovery(self) -> None: + def resume_from_recovery(self, reconcile_false_positive: bool) -> None: """Resume the run from recovery mode.""" - self.run_orchestrator.resume_from_recovery() + self.run_orchestrator.resume_from_recovery(reconcile_false_positive) async def finish(self, error: Optional[Exception]) -> None: """Finish the run.""" diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml index e89681be0ac..27f894fe411 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml @@ -101,7 +101,7 @@ stages: startedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" completedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" status: failed - notes: [] + notes: !anylist error: id: !anystr errorType: TipNotAttachedError diff --git a/robot-server/tests/runs/test_error_recovery_mapping.py b/robot-server/tests/runs/test_error_recovery_mapping.py index a125d12649d..8b75ff99aad 100644 --- a/robot-server/tests/runs/test_error_recovery_mapping.py +++ b/robot-server/tests/runs/test_error_recovery_mapping.py @@ -72,7 +72,7 @@ def test_create_error_recovery_policy_with_rules( mock_error_data: CommandDefinedErrorData, mock_rule: ErrorRecoveryRule, ) -> None: - """Should return IGNORE_AND_CONTINUE if that's what we specify as the rule.""" + """Should return CONTINUE_WITH_ERROR if we specified IGNORE_AND_CONTINUE as the rule.""" policy = create_error_recovery_policy_from_rules([mock_rule], enabled=True) example_config = Config( robot_type="OT-3 Standard", @@ -80,7 +80,7 @@ def test_create_error_recovery_policy_with_rules( ) assert ( policy(example_config, mock_command, mock_error_data) - == ErrorRecoveryType.IGNORE_AND_CONTINUE + == ErrorRecoveryType.CONTINUE_WITH_ERROR ) @@ -141,7 +141,7 @@ def test_enabled_boolean(enabled: bool) -> None: policy = create_error_recovery_policy_from_rules(rules, enabled) result = policy(example_config, command, error_data) expected_result = ( - ErrorRecoveryType.IGNORE_AND_CONTINUE if enabled else ErrorRecoveryType.FAIL_RUN + ErrorRecoveryType.CONTINUE_WITH_ERROR if enabled else ErrorRecoveryType.FAIL_RUN ) assert result == expected_result @@ -187,7 +187,7 @@ def test_enabled_on_flex_disabled_on_ot2( policy = create_error_recovery_policy_from_rules(rules, enabled=True) result = policy(example_config, command, error_data) expected_result = ( - ErrorRecoveryType.IGNORE_AND_CONTINUE + ErrorRecoveryType.CONTINUE_WITH_ERROR if expect_error_recovery_to_be_enabled else ErrorRecoveryType.FAIL_RUN ) diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index b069632a4e4..7dbdf827012 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -304,7 +304,9 @@ def test_create_resume_from_recovery_action( ) decoy.verify(mock_run_store.insert_action(run_id, result), times=1) - decoy.verify(mock_run_orchestrator_store.resume_from_recovery()) + decoy.verify( + mock_run_orchestrator_store.resume_from_recovery(reconcile_false_positive=False) + ) @pytest.mark.parametrize( diff --git a/shared-data/js/getLabware.ts b/shared-data/js/getLabware.ts index be1daebb7ea..6fd22f1f5d4 100644 --- a/shared-data/js/getLabware.ts +++ b/shared-data/js/getLabware.ts @@ -57,6 +57,8 @@ export const PD_DO_NOT_LIST = [ 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep', 'opentrons_96_pcr_adapter_armadillo_wellplate_200ul', + // temporarily blocking TC lid adapter until it is supported in PD + 'opentrons_tough_pcr_auto_sealing_lid', ] export function getIsLabwareV1Tiprack(def: LabwareDefinition1): boolean { diff --git a/yarn.lock b/yarn.lock index 29b6725a35f..9fecdf8fa95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5692,13 +5692,6 @@ dependencies: "@types/react" "*" -"@types/react-lottie@^1.2.10": - version "1.2.10" - resolved "https://registry.yarnpkg.com/@types/react-lottie/-/react-lottie-1.2.10.tgz#220f68a2dfa0d4b131ab4930e8bf166b9442c68c" - integrity sha512-rCd1p3US4ELKJlqwVnP0h5b24zt5p9OCvKUoNpYExLqwbFZMWEiJ6EGLMmH7nmq5V7KomBIbWO2X/XRFsL0vCA== - dependencies: - "@types/react" "*" - "@types/react-redux@7.1.32": version "7.1.32" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.32.tgz#bf162289e0c69e44a649dfcadb30f7f7c4cb00e4" @@ -7182,7 +7175,7 @@ babel-plugin-unassert@^3.0.1: resolved "https://registry.yarnpkg.com/babel-plugin-unassert/-/babel-plugin-unassert-3.2.0.tgz#4ea8f65709905cc540627baf4ce4c837281a317d" integrity sha512-dNeuFtaJ1zNDr59r24NjjIm4SsXXm409iNOVMIERp6ePciII+rTrdwsWcHDqDFUKpOoBNT4ZS63nPEbrANW7DQ== -babel-runtime@6.x.x, babel-runtime@^6.26.0: +babel-runtime@6.x.x: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== @@ -14957,11 +14950,6 @@ lost@^8.3.1: object-assign "^4.1.1" postcss "7.0.14" -lottie-web@^5.1.3: - version "5.12.2" - resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.12.2.tgz#579ca9fe6d3fd9e352571edd3c0be162492f68e5" - integrity sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg== - loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -18592,14 +18580,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-lottie@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.4.tgz#999ccabff8afc82074588bc50bd75be6f8945161" - integrity sha512-kBGxI+MIZGBf4wZhNCWwHkMcVP+kbpmrLWH/SkO0qCKc7D7eSPcxQbfpsmsCo8v2KCBYjuGSou+xTqK44D/jMg== - dependencies: - babel-runtime "^6.26.0" - lottie-web "^5.1.3" - react-markdown@9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1"