diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8758cabe..0d84b9a1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -102,6 +102,28 @@ pytest: - tags - web +openapi: + stage: testing + image: python:3.7 + retry: 1 + + <<: *python-install + + script: + - mkdir -p docs/build/ + - ofm-generate-openapi -o docs/build/swagger.yaml --validate + artifacts: + name: "openapi" + expire_in: 1 week + paths: + - "./docs/build/swagger.yaml" + + only: + - branches + - merge_requests + - tags + - web + # JavaScript linting with ESLint (via Vue CLI) eslint: stage: analysis @@ -146,6 +168,7 @@ package: stage: package dependencies: - build + - openapi image: ubuntu:latest @@ -154,9 +177,11 @@ package: - mkdir -p dist - tar -c --exclude-vcs --exclude-from .tarignore -vzf dist/openflexure-microscope-server-${CI_COMMIT_REF_NAME}.tar.gz . - tar -c --exclude-vcs --exclude-from .tarignore -vzf dist/openflexure-microscope-webapp-${CI_COMMIT_REF_NAME}.tar.gz ./openflexure_microscope/api/static/dist/ + - cp docs/build/swagger.yaml dist/openflexure-microscope-api-${CI_COMMIT_REF_NAME}.yaml - cd dist/ - sha256sum openflexure-microscope-server-${CI_COMMIT_REF_NAME}.tar.gz > openflexure-microscope-server-${CI_COMMIT_REF_NAME}.tar.gz.sha256 - sha256sum openflexure-microscope-webapp-${CI_COMMIT_REF_NAME}.tar.gz > openflexure-microscope-webapp-${CI_COMMIT_REF_NAME}.tar.gz.sha256 + - sha256sum openflexure-microscope-api-${CI_COMMIT_REF_NAME}.yaml > openflexure-microscope-api-${CI_COMMIT_REF_NAME}.yaml.sha256 artifacts: name: "dist" diff --git a/Pipfile.lock b/Pipfile.lock index d3a660f7..04c8a27a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -23,14 +23,15 @@ "default": { "apispec": { "extras": [ + "validation", "yaml" ], "hashes": [ - "sha256:aaded4ae451752c58b70e579923d8db7daf0b71f62def8c8fe7a2a52bd7c06a2", - "sha256:f49c49e0dee3fa545462d4536856458aae61b972727b3c32eabc7d6db8c074f8" + "sha256:6613dbc39f41cd58942a697f11c8762ba18422bd173fe0bdfc1535b83d3f84f0", + "sha256:79029486d36a0d7f3c659dbf6ae50a91fbed0c22dcd5376f592e076c130bc7f9" ], "markers": "python_version >= '3.6'", - "version": "==4.4.1" + "version": "==4.7.1" }, "apispec-webframeworks": { "hashes": [ @@ -53,9 +54,32 @@ "sha256:02a409bd2cb193b20d9eb015ca31400bd3e1721ec5d702e1b96de54608ff5a8f", "sha256:d39c3afebb59ec865fb1247dfd951d7090c159ab0cf3e4291e0869d3be33776c" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==0.1.4" }, + "certifi": { + "hashes": [ + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + ], + "version": "==2021.5.30" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1", + "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12" + ], + "markers": "python_version >= '3'", + "version": "==2.0.3" + }, "click": { "hashes": [ "sha256:a3747c864f8e400a3664f5f4fd6dae11b4605bf6b727dae7b6f22ba9bd0a194a", @@ -127,7 +151,7 @@ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.5" }, "expiringdict": { @@ -160,6 +184,14 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, + "idna": { + "hashes": [ + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + ], + "markers": "python_version >= '3'", + "version": "==3.2" + }, "ifaddr": { "hashes": [ "sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94", @@ -169,11 +201,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", + "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" ], "markers": "python_version < '3.8'", - "version": "==4.0.1" + "version": "==4.6.1" }, "iniconfig": { "hashes": [ @@ -183,6 +215,13 @@ ], "version": "==1.1.1" }, + "isodate": { + "hashes": [ + "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", + "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81" + ], + "version": "==0.6.0" + }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", @@ -199,13 +238,20 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.3" }, + "jsonschema": { + "hashes": [ + "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", + "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" + ], + "version": "==3.2.0" + }, "labthings": { "hashes": [ - "sha256:4f286ea585e596aeb67a02d5bd822b2c89828693026085829e15e415a2d2d287", - "sha256:970bb6a81f0e007b55e575e6eafe6bba10974d671f2767b91631f001ff53cf0e" + "sha256:2a167d512fe8f01283a46753c070a75778f8d573c8291ce6398c236029b85891", + "sha256:a63f410dd39273af8b774434b20457c367548990f19340360397bba26dfd4ca7" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==1.2.4" + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==1.3.0" }, "markupsafe": { "hashes": [ @@ -250,74 +296,95 @@ }, "marshmallow": { "hashes": [ - "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040", - "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01" + "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e", + "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842", + "sha256:f9b83845790eae045b4cfa220f9118b1b7db166d02db90e7a82d28eed401da11" ], "markers": "python_version >= '3.5'", - "version": "==3.12.1" + "version": "==3.13.0" }, "numpy": { "hashes": [ - "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", - "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", - "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", - "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", - "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", - "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", - "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", - "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", - "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", - "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", - "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", - "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", - "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", - "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", - "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", - "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", - "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", - "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", - "sha256:d840d80623c96696fab0d15489f49425bec1c0f47f3bf0b3362ce0c75dbad993", - "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", - "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", - "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", - "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", - "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", - "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" + "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33", + "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5", + "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1", + "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1", + "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac", + "sha256:2c6413051a74c317d36f4bac04f61e8d1a6708969cc910d876664c76e36817a7", + "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4", + "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50", + "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6", + "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267", + "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172", + "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af", + "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8", + "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2", + "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63", + "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1", + "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8", + "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16", + "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214", + "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd", + "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68", + "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062", + "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e", + "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f", + "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b", + "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd", + "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671", + "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a", + "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a" ], "markers": "python_version >= '3.7'", - "version": "==1.20.3" + "version": "==1.21.1" + }, + "openapi-schema-validator": { + "hashes": [ + "sha256:215b516d0942f4e8e2446cf3f7d4ff2ed71d102ebddcc30526d8a3f706ab1df6", + "sha256:a4b2712020284cee880b4c55faa513fbc2f8f07f365deda6098f8ab943c9f0df", + "sha256:b65d6c2242620bfe76d4c749b61cd9657e4528895a8f4fb6f916085b508ebd24" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.1.5" + }, + "openapi-spec-validator": { + "hashes": [ + "sha256:0a7da925bad4576f4518f77302c0b1990adb2fbcbe7d63fb4ed0de894cad8bdd", + "sha256:3d70e6592754799f7e77a45b98c6a91706bdd309a425169d17d8e92173e198a2", + "sha256:ba28b06e63274f2bc6de995a07fb572c657e534425b5baf68d9f7911efe6929f" + ], + "version": "==0.3.1" }, "opencv-python-headless": { "hashes": [ - "sha256:0e02809db2968e54f3c23340be6ff9a1428b3f03f5dca7cd5aceda66e319ce86", - "sha256:1f7c14f5d4e5af4dc4669fc6b4a983b36072a934c439ac11266b930496da8255", - "sha256:243aea91cc1e36a47c46da4cc408071af39444a48df1fe1539ea8d7990500fd2", - "sha256:2560dcf3c1158226b066302f777bfe0f65282410b8d90871dd872306c967d1f1", - "sha256:2e6a9a88617a0ef7219cff24ba78a58416670a77e6ac63975f9009af3319ab63", - "sha256:372149d007e20bf556b7687591d22b58b56b3c225f492da051d81587e5dc7411", - "sha256:522f12dd994e064a30562adfd63b9439099bd7c80819f5261c37ebe593283c9e", - "sha256:526b9e19cf6300f0891d8f427eac1048091912332bb781eb4957a4994bfbe608", - "sha256:5f0c7d34fa9953706c2a1a6d2760a91dc5a68cab3df16af61609894c6c8586f1", - "sha256:657cdd9fbbbbb7e898ae3d9b0649b367d0e440d429c714104d069c5612d578bb", - "sha256:65c9ea57be5ebcaed009b3fee14fe59f3b6aa1e573fe08c5039ff057ad593c53", - "sha256:777fb596e04331f73ef5b0c1faa4d33348f29ca58216d9286355c16f5489c939", - "sha256:7c75680ffdf32d7044415a215d1fc60dbec14a7f2f0b59a85f0f74ef5efcb6ad", - "sha256:924aa4c34ead0b817309f42291dc526b2a7755476afe3009d1e275fc3090d92d", - "sha256:96d1da6ad061d8f3509668d398d14dd8265e529d6407f87eb26e7f4ddf043cc0", - "sha256:9aa04a491c534531029fbac61da961fae0bf4abb1786eed9c91befde4ca7bd81", - "sha256:a322d4df14c3a5f19701a023b3da91e9b8af8653b0d2ee0c50b0b341212f7343", - "sha256:aa562c520f46283423ba8fac29099458e42deab697a9abd0491622e421c5c454", - "sha256:ba2f0bd46e9534f29969e39f7895cbea9764173102b0c04b7818b8a9910d66e4", - "sha256:d16825755e7b5a6d8737f93e116670229e1510199e0af9213004e187ae0dbcc5", - "sha256:e3027e0d1b71b68b5cfe1ea9c627e323dff71112c854ba19805258d8fc6c630e", - "sha256:e61c8df818d79c6bd62a5a18a9b3bfdf389502798e3c22f95c67f8dcd09ce97b", - "sha256:f2011ecb3980bbed283d17d43e0f1221bac88c0cac1a6fb59a056544de2df2f7", - "sha256:f5e40a06116460ef2fd2d1c24be3b65f8bfb5fcdfe433f3fc01bcb4c2eb485bf", - "sha256:f7f8e4f7c63c8e95eb210f4cba88d0069a0a964d6335d7a35b07f0d0baa13558", - "sha256:fe02a943b1a28b505e954fbce24e867119a3eb4351f93adad55c6cfe81a70484" + "sha256:0935bf569e2edc1af8bc38f00813e37b1ba63e4b2eb0dc48651f405068b8b36d", + "sha256:0e8080dc5baf6855b663634d9c7a45732e2999bbe44aa5e4a14c15f3631998e4", + "sha256:0ef768d119a00eb6be0ba7953ab875218084d078db564af89af2b3891b6d7a3b", + "sha256:0efe30ce934ec2ab7272143d17f68885910a715c6aeffd02973c04b3d8b99ca6", + "sha256:1a00d01d8d7250d6b87c63eb7b2df1e541fdb10919c22940cc53884bb12057c9", + "sha256:3308d22efec8a69932709fc76456ef7e0d1cd9eb2272d94b7e5df0674d3e324f", + "sha256:3599939dc61ab32c91cb2dbfd80a8199b53bf8a08cbb636ee9c2f27d74fe4f8f", + "sha256:39eaae19d5fa906c7ec2db76447c0ff9df6305633493f1b86e4c0080ed1cb036", + "sha256:3b1aa94894872e17a5fa07794562d8c44add78dd4c90a12fa1d946789e088784", + "sha256:465b157480902ecd6b2c4c46d547e4cfdec7595177bcc612c6f4c351966a5a1e", + "sha256:90686bd1292972ad47298154c8ad37543080b2fc0417b68a350d30232b77f670", + "sha256:b224afffbc4ea21fe100824ff08a79ad1fae3088c1280b8ff815067b827e5240", + "sha256:b563106e93525bd30072a27448cc2b5c11e45bf6025e74e1d40efd5349a31736", + "sha256:bfe8989ed4af5e14ef1480c89fe259f5d4bc66c5ae0e7d56d7c778ef6ebbcdf4", + "sha256:c54ad51bfc4623de287bd68f14f481f72300bd16a2d9e14850c1ef1a2dfc7dca", + "sha256:c7a1d365f2f8ce413847479a2d39bee2be6d5027119b08c4c5f6594f1f57673d", + "sha256:d4c784339c366d27d0b4503a073c97a7cd78657937b3755fdbc9168a002f6409", + "sha256:d70f3a44fb25f555da36e6c242cd1dffac6c4637eb900584f2a70a46e8e3cf05", + "sha256:e19ba664cae0bc6ad39e2fbf3b3f16c93009132f792dcb7ca0105f0cc15997ed", + "sha256:ebfeaff7e0bb1c293008a49b2b2c033a7cec3805006a8284fa1a44ad8c30446b", + "sha256:ecd84834f58daf94d3a4874a11997180380ef9af11b1fd5f81dbb8599b3c3446", + "sha256:f1082f4f15a681b435686ed3fc0232128a222fed6792a47fae5cf081eb751ccf", + "sha256:fc9fa0fa88423c0e74db52fe13939aa506789f924a2a879e31f1d415e39a0db2", + "sha256:fd0f0629e27d97eec2f51500afbe0845876fdcd89d412035f4d7beb618c51066", + "sha256:ff4de00d4f19d622561dee58a5121045ca265df3665751d14433149bbd748f55" ], "markers": "python_version >= '3.6'", - "version": "==4.5.1.48" + "version": "==4.5.3.56" }, "openflexure-microscope-server": { "editable": true, @@ -325,11 +392,11 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.0" }, "picamerax": { "hashes": [ @@ -356,6 +423,7 @@ "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", "sha256:39b8830992872e00605ec6eb0d83efad488553f10d79066b8b47c8be38e126b3", "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", + "sha256:4a2ef919795ad6fc28c47be3f16ec8add4e8c6daa01871e25788cda1b244b162", "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", @@ -390,6 +458,16 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, + "prance": { + "extras": [ + "osv" + ], + "hashes": [ + "sha256:03374b020a4f643c71fc941bb7d1e4852331732714b4b6ebbbd53dd5f75adacf", + "sha256:43ebe3a5b38f0c65c428427004c4d8ce8d7155ddad50610276c89c192680f138" + ], + "version": "==0.21.2" + }, "psutil": { "hashes": [ "sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b", @@ -398,6 +476,7 @@ "sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995", "sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd", "sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73", + "sha256:7d5e1be385287e2334f708ec261b8ee364ac2bcb07eadcbd7a254f9cd476b65f", "sha256:891e24ec41be15a99c00b0f3a378d3d439876fe18793f874a4f8e796fec77458", "sha256:a9340f6b9ff887d77aed72a51d101ca0f21b8d82edc6c85105d1d9f027544365", "sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465", @@ -425,6 +504,34 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, + "pyrsistent": { + "hashes": [ + "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", + "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", + "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", + "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", + "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", + "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", + "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", + "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", + "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", + "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", + "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", + "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", + "sha256:8c065dcc8ed1c7412be6f0cfa69e49434c61bf73953ee30ddf35420795408e47", + "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", + "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", + "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", + "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", + "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", + "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", + "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", + "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", + "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" + ], + "markers": "python_version >= '3.6'", + "version": "==0.18.0" + }, "pyserial": { "hashes": [ "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", @@ -450,11 +557,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.1" + "version": "==2.8.2" }, "pyyaml": { "hashes": [ @@ -489,17 +596,16 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==5.4.1" }, - "rpi.gpio": { + "requests": { "hashes": [ - "sha256:5cd2f3c88d0b1e2f4780c905702c0474768c2bc113fb793121c2e117b401ab16", - "sha256:6a4791f41cafc2ee6e4cb70e5bd31fadc66a0cfab29b38df8723a98f6f73ad5a", - "sha256:7424bc6c205466764f30f666c18187a0824077daf20b295c42f08aea2cb87d3f", - "sha256:7da5235aeba20da39ad4fb97f3ba3a2c6b2d60bba8958b00a4df42f608c6d42e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "markers": "platform_machine == 'armv7l'", - "version": "==0.7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" }, "sangaboard": { "hashes": [ @@ -534,6 +640,14 @@ "markers": "python_version < '3.10' and python_version >= '3.7'", "version": "==1.6.3" }, + "semver": { + "hashes": [ + "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4", + "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.13.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -559,13 +673,21 @@ "markers": "python_version < '3.8'", "version": "==3.7.4.3" }, + "urllib3": { + "hashes": [ + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.6" + }, "webargs": { "hashes": [ - "sha256:2f3d883ce9f348fa884889440fcc1b207e7c67b04be5e90be00a5be73e2cd91d", - "sha256:ce8c565789ece1584be7edba41617319eb75de59b2987958bee3f3fdb3669e60" + "sha256:565666bafd08dd375fae995a9d4187604741efc27b8ec7c5b5bcb0ae90b2c677", + "sha256:7b06eebf23d05ebec1aa97e9c373deadaa8194866fae79c56e0453804db7a756" ], "markers": "python_version >= '3.6'", - "version": "==7.0.1" + "version": "==8.0.0" }, "werkzeug": { "hashes": [ @@ -577,18 +699,19 @@ }, "zeroconf": { "hashes": [ - "sha256:3608be2db58f6f0dc70665e02ab420fb8bf428016f2c78403d879e066ecc9bff", - "sha256:4be24a10aa9c73406f48d42a8b3b077c217b0e8d7ed1e57639630da520c25959" + "sha256:46992d0786129c769c652c875221d394a63bdbef12832d443ac9e521005d46e1", + "sha256:ac88738639669f924859a4e01d9742f3783e619e710103f4a0d321a47f260fd2", + "sha256:de14268be7a449691f74463cce5ecbeeb7e5f572878a28285b11236b0171d407" ], - "version": "==0.28.8" + "version": "==0.33.1" }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" ], "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "version": "==3.5.0" } }, "develop": { @@ -601,14 +724,15 @@ }, "apispec": { "extras": [ + "validation", "yaml" ], "hashes": [ - "sha256:aaded4ae451752c58b70e579923d8db7daf0b71f62def8c8fe7a2a52bd7c06a2", - "sha256:f49c49e0dee3fa545462d4536856458aae61b972727b3c32eabc7d6db8c074f8" + "sha256:6613dbc39f41cd58942a697f11c8762ba18422bd173fe0bdfc1535b83d3f84f0", + "sha256:79029486d36a0d7f3c659dbf6ae50a91fbed0c22dcd5376f592e076c130bc7f9" ], "markers": "python_version >= '3.6'", - "version": "==4.4.1" + "version": "==4.7.1" }, "apispec-webframeworks": { "hashes": [ @@ -662,15 +786,15 @@ "sha256:02a409bd2cb193b20d9eb015ca31400bd3e1721ec5d702e1b96de54608ff5a8f", "sha256:d39c3afebb59ec865fb1247dfd951d7090c159ab0cf3e4291e0869d3be33776c" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==0.1.4" }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2020.12.5" + "version": "==2021.5.30" }, "chardet": { "hashes": [ @@ -680,6 +804,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.0.0" }, + "charset-normalizer": { + "hashes": [ + "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1", + "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12" + ], + "markers": "python_version >= '3'", + "version": "==2.0.3" + }, "click": { "hashes": [ "sha256:a3747c864f8e400a3664f5f4fd6dae11b4605bf6b727dae7b6f22ba9bd0a194a", @@ -751,16 +883,16 @@ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.5" }, "docutils": { "hashes": [ - "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", - "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.17.1" + "version": "==0.16" }, "expiringdict": { "hashes": [ @@ -802,12 +934,11 @@ }, "idna": { "hashes": [ - "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226", - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.2" }, "ifaddr": { "hashes": [ @@ -826,11 +957,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", + "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" ], "markers": "python_version < '3.8'", - "version": "==4.0.1" + "version": "==4.6.1" }, "iniconfig": { "hashes": [ @@ -840,13 +971,20 @@ ], "version": "==1.1.1" }, + "isodate": { + "hashes": [ + "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", + "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81" + ], + "version": "==0.6.0" + }, "isort": { "hashes": [ - "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", - "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" + "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813", + "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e" ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==5.8.0" + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "version": "==5.9.2" }, "itsdangerous": { "hashes": [ @@ -864,13 +1002,20 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.3" }, + "jsonschema": { + "hashes": [ + "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", + "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" + ], + "version": "==3.2.0" + }, "labthings": { "hashes": [ - "sha256:4f286ea585e596aeb67a02d5bd822b2c89828693026085829e15e415a2d2d287", - "sha256:970bb6a81f0e007b55e575e6eafe6bba10974d671f2767b91631f001ff53cf0e" + "sha256:2a167d512fe8f01283a46753c070a75778f8d573c8291ce6398c236029b85891", + "sha256:a63f410dd39273af8b774434b20457c367548990f19340360397bba26dfd4ca7" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==1.2.4" + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==1.3.0" }, "lazy-object-proxy": { "hashes": [ @@ -955,6 +1100,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.6.3" }, + "m2r": { + "hashes": [ + "sha256:4c72974da78f787a6b0b83973177479b17d311e2d04f6a38998c677e8f394129", + "sha256:bf90bad66cda1164b17e5ba4a037806d2443f2a4d5ddc9f6a5554a0322aaed99" + ], + "version": "==0.2.1" + }, "markupsafe": { "hashes": [ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", @@ -998,11 +1150,12 @@ }, "marshmallow": { "hashes": [ - "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040", - "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01" + "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e", + "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842", + "sha256:f9b83845790eae045b4cfa220f9118b1b7db166d02db90e7a82d28eed401da11" ], "markers": "python_version >= '3.5'", - "version": "==3.12.1" + "version": "==3.13.0" }, "mccabe": { "hashes": [ @@ -1011,33 +1164,41 @@ ], "version": "==0.6.1" }, + "mistune": { + "hashes": [ + "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", + "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4" + ], + "version": "==0.8.4" + }, "mypy": { "hashes": [ - "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", - "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", - "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", - "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", - "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", - "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", - "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", - "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", - "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", - "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", - "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", - "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", - "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", - "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", - "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", - "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", - "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", - "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", - "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", - "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", - "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", - "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" + "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", + "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", + "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", + "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", + "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", + "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", + "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", + "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", + "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", + "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", + "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", + "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", + "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", + "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", + "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", + "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", + "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", + "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", + "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", + "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", + "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", + "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", + "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" ], "markers": "python_version >= '3.5'", - "version": "==0.812" + "version": "==0.910" }, "mypy-extensions": { "hashes": [ @@ -1048,66 +1209,86 @@ }, "numpy": { "hashes": [ - "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", - "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", - "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", - "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", - "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", - "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", - "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", - "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", - "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", - "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", - "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", - "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", - "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", - "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", - "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", - "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", - "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", - "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", - "sha256:d840d80623c96696fab0d15489f49425bec1c0f47f3bf0b3362ce0c75dbad993", - "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", - "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", - "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", - "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", - "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", - "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" + "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33", + "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5", + "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1", + "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1", + "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac", + "sha256:2c6413051a74c317d36f4bac04f61e8d1a6708969cc910d876664c76e36817a7", + "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4", + "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50", + "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6", + "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267", + "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172", + "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af", + "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8", + "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2", + "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63", + "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1", + "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8", + "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16", + "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214", + "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd", + "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68", + "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062", + "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e", + "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f", + "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b", + "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd", + "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671", + "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a", + "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a" ], "markers": "python_version >= '3.7'", - "version": "==1.20.3" + "version": "==1.21.1" + }, + "openapi-schema-validator": { + "hashes": [ + "sha256:215b516d0942f4e8e2446cf3f7d4ff2ed71d102ebddcc30526d8a3f706ab1df6", + "sha256:a4b2712020284cee880b4c55faa513fbc2f8f07f365deda6098f8ab943c9f0df", + "sha256:b65d6c2242620bfe76d4c749b61cd9657e4528895a8f4fb6f916085b508ebd24" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.1.5" + }, + "openapi-spec-validator": { + "hashes": [ + "sha256:0a7da925bad4576f4518f77302c0b1990adb2fbcbe7d63fb4ed0de894cad8bdd", + "sha256:3d70e6592754799f7e77a45b98c6a91706bdd309a425169d17d8e92173e198a2", + "sha256:ba28b06e63274f2bc6de995a07fb572c657e534425b5baf68d9f7911efe6929f" + ], + "version": "==0.3.1" }, "opencv-python-headless": { "hashes": [ - "sha256:0e02809db2968e54f3c23340be6ff9a1428b3f03f5dca7cd5aceda66e319ce86", - "sha256:1f7c14f5d4e5af4dc4669fc6b4a983b36072a934c439ac11266b930496da8255", - "sha256:243aea91cc1e36a47c46da4cc408071af39444a48df1fe1539ea8d7990500fd2", - "sha256:2560dcf3c1158226b066302f777bfe0f65282410b8d90871dd872306c967d1f1", - "sha256:2e6a9a88617a0ef7219cff24ba78a58416670a77e6ac63975f9009af3319ab63", - "sha256:372149d007e20bf556b7687591d22b58b56b3c225f492da051d81587e5dc7411", - "sha256:522f12dd994e064a30562adfd63b9439099bd7c80819f5261c37ebe593283c9e", - "sha256:526b9e19cf6300f0891d8f427eac1048091912332bb781eb4957a4994bfbe608", - "sha256:5f0c7d34fa9953706c2a1a6d2760a91dc5a68cab3df16af61609894c6c8586f1", - "sha256:657cdd9fbbbbb7e898ae3d9b0649b367d0e440d429c714104d069c5612d578bb", - "sha256:65c9ea57be5ebcaed009b3fee14fe59f3b6aa1e573fe08c5039ff057ad593c53", - "sha256:777fb596e04331f73ef5b0c1faa4d33348f29ca58216d9286355c16f5489c939", - "sha256:7c75680ffdf32d7044415a215d1fc60dbec14a7f2f0b59a85f0f74ef5efcb6ad", - "sha256:924aa4c34ead0b817309f42291dc526b2a7755476afe3009d1e275fc3090d92d", - "sha256:96d1da6ad061d8f3509668d398d14dd8265e529d6407f87eb26e7f4ddf043cc0", - "sha256:9aa04a491c534531029fbac61da961fae0bf4abb1786eed9c91befde4ca7bd81", - "sha256:a322d4df14c3a5f19701a023b3da91e9b8af8653b0d2ee0c50b0b341212f7343", - "sha256:aa562c520f46283423ba8fac29099458e42deab697a9abd0491622e421c5c454", - "sha256:ba2f0bd46e9534f29969e39f7895cbea9764173102b0c04b7818b8a9910d66e4", - "sha256:d16825755e7b5a6d8737f93e116670229e1510199e0af9213004e187ae0dbcc5", - "sha256:e3027e0d1b71b68b5cfe1ea9c627e323dff71112c854ba19805258d8fc6c630e", - "sha256:e61c8df818d79c6bd62a5a18a9b3bfdf389502798e3c22f95c67f8dcd09ce97b", - "sha256:f2011ecb3980bbed283d17d43e0f1221bac88c0cac1a6fb59a056544de2df2f7", - "sha256:f5e40a06116460ef2fd2d1c24be3b65f8bfb5fcdfe433f3fc01bcb4c2eb485bf", - "sha256:f7f8e4f7c63c8e95eb210f4cba88d0069a0a964d6335d7a35b07f0d0baa13558", - "sha256:fe02a943b1a28b505e954fbce24e867119a3eb4351f93adad55c6cfe81a70484" + "sha256:0935bf569e2edc1af8bc38f00813e37b1ba63e4b2eb0dc48651f405068b8b36d", + "sha256:0e8080dc5baf6855b663634d9c7a45732e2999bbe44aa5e4a14c15f3631998e4", + "sha256:0ef768d119a00eb6be0ba7953ab875218084d078db564af89af2b3891b6d7a3b", + "sha256:0efe30ce934ec2ab7272143d17f68885910a715c6aeffd02973c04b3d8b99ca6", + "sha256:1a00d01d8d7250d6b87c63eb7b2df1e541fdb10919c22940cc53884bb12057c9", + "sha256:3308d22efec8a69932709fc76456ef7e0d1cd9eb2272d94b7e5df0674d3e324f", + "sha256:3599939dc61ab32c91cb2dbfd80a8199b53bf8a08cbb636ee9c2f27d74fe4f8f", + "sha256:39eaae19d5fa906c7ec2db76447c0ff9df6305633493f1b86e4c0080ed1cb036", + "sha256:3b1aa94894872e17a5fa07794562d8c44add78dd4c90a12fa1d946789e088784", + "sha256:465b157480902ecd6b2c4c46d547e4cfdec7595177bcc612c6f4c351966a5a1e", + "sha256:90686bd1292972ad47298154c8ad37543080b2fc0417b68a350d30232b77f670", + "sha256:b224afffbc4ea21fe100824ff08a79ad1fae3088c1280b8ff815067b827e5240", + "sha256:b563106e93525bd30072a27448cc2b5c11e45bf6025e74e1d40efd5349a31736", + "sha256:bfe8989ed4af5e14ef1480c89fe259f5d4bc66c5ae0e7d56d7c778ef6ebbcdf4", + "sha256:c54ad51bfc4623de287bd68f14f481f72300bd16a2d9e14850c1ef1a2dfc7dca", + "sha256:c7a1d365f2f8ce413847479a2d39bee2be6d5027119b08c4c5f6594f1f57673d", + "sha256:d4c784339c366d27d0b4503a073c97a7cd78657937b3755fdbc9168a002f6409", + "sha256:d70f3a44fb25f555da36e6c242cd1dffac6c4637eb900584f2a70a46e8e3cf05", + "sha256:e19ba664cae0bc6ad39e2fbf3b3f16c93009132f792dcb7ca0105f0cc15997ed", + "sha256:ebfeaff7e0bb1c293008a49b2b2c033a7cec3805006a8284fa1a44ad8c30446b", + "sha256:ecd84834f58daf94d3a4874a11997180380ef9af11b1fd5f81dbb8599b3c3446", + "sha256:f1082f4f15a681b435686ed3fc0232128a222fed6792a47fae5cf081eb751ccf", + "sha256:fc9fa0fa88423c0e74db52fe13939aa506789f924a2a879e31f1d415e39a0db2", + "sha256:fd0f0629e27d97eec2f51500afbe0845876fdcd89d412035f4d7beb618c51066", + "sha256:ff4de00d4f19d622561dee58a5121045ca265df3665751d14433149bbd748f55" ], "markers": "python_version >= '3.6'", - "version": "==4.5.1.48" + "version": "==4.5.3.56" }, "openflexure-microscope-server": { "editable": true, @@ -1115,11 +1296,11 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.0" }, "pastel": { "hashes": [ @@ -1154,6 +1335,7 @@ "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", "sha256:39b8830992872e00605ec6eb0d83efad488553f10d79066b8b47c8be38e126b3", "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", + "sha256:4a2ef919795ad6fc28c47be3f16ec8add4e8c6daa01871e25788cda1b244b162", "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", @@ -1193,9 +1375,19 @@ "sha256:6fb3021603d4421c6fcc40072bbcf150a6c52ef70ff4d3be089b8b04e015ef5a", "sha256:70b97cb194b978dc464c70793e85e6f746cddf82b84a38bfb135946ad71ae19c" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==0.10.0" }, + "prance": { + "extras": [ + "osv" + ], + "hashes": [ + "sha256:03374b020a4f643c71fc941bb7d1e4852331732714b4b6ebbbd53dd5f75adacf", + "sha256:43ebe3a5b38f0c65c428427004c4d8ce8d7155ddad50610276c89c192680f138" + ], + "version": "==0.21.2" + }, "psutil": { "hashes": [ "sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b", @@ -1204,6 +1396,7 @@ "sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995", "sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd", "sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73", + "sha256:7d5e1be385287e2334f708ec261b8ee364ac2bcb07eadcbd7a254f9cd476b65f", "sha256:891e24ec41be15a99c00b0f3a378d3d439876fe18793f874a4f8e796fec77458", "sha256:a9340f6b9ff887d77aed72a51d101ca0f21b8d82edc6c85105d1d9f027544365", "sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465", @@ -1233,11 +1426,11 @@ }, "pylint": { "hashes": [ - "sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217", - "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b" + "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8", + "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484" ], "markers": "python_version ~= '3.6'", - "version": "==2.8.2" + "version": "==2.8.3" }, "pyparsing": { "hashes": [ @@ -1247,6 +1440,34 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, + "pyrsistent": { + "hashes": [ + "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", + "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", + "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", + "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", + "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", + "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", + "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", + "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", + "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", + "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", + "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", + "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", + "sha256:8c065dcc8ed1c7412be6f0cfa69e49434c61bf73953ee30ddf35420795408e47", + "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", + "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", + "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", + "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", + "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", + "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", + "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", + "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", + "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" + ], + "markers": "python_version >= '3.6'", + "version": "==0.18.0" + }, "pyserial": { "hashes": [ "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", @@ -1272,11 +1493,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.1" + "version": "==2.8.2" }, "pytz": { "hashes": [ @@ -1318,15 +1539,16 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==5.4.1" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.25.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" }, "rope": { "hashes": [ @@ -1336,16 +1558,6 @@ ], "version": "==0.14.0" }, - "rpi.gpio": { - "hashes": [ - "sha256:5cd2f3c88d0b1e2f4780c905702c0474768c2bc113fb793121c2e117b401ab16", - "sha256:6a4791f41cafc2ee6e4cb70e5bd31fadc66a0cfab29b38df8723a98f6f73ad5a", - "sha256:7424bc6c205466764f30f666c18187a0824077daf20b295c42f08aea2cb87d3f", - "sha256:7da5235aeba20da39ad4fb97f3ba3a2c6b2d60bba8958b00a4df42f608c6d42e" - ], - "markers": "platform_machine == 'armv7l'", - "version": "==0.7.0" - }, "sangaboard": { "hashes": [ "sha256:3791159d57a749571f89acdce70aa9c1de82afef71f5186d5e2845c7be44698c", @@ -1379,6 +1591,14 @@ "markers": "python_version < '3.10' and python_version >= '3.7'", "version": "==1.6.3" }, + "semver": { + "hashes": [ + "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4", + "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.13.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -1396,11 +1616,18 @@ }, "sphinx": { "hashes": [ - "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c", - "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4" + "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1", + "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.2" + "markers": "python_version >= '3.5'", + "version": "==3.5.4" + }, + "sphinx-rtd-theme": { + "hashes": [ + "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", + "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" + ], + "version": "==0.5.2" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -1420,11 +1647,11 @@ }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", - "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" + "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", + "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" + "markers": "python_version >= '3.6'", + "version": "==2.0.0" }, "sphinxcontrib-httpdomain": { "hashes": [ @@ -1441,6 +1668,14 @@ "markers": "python_version >= '3.5'", "version": "==1.0.1" }, + "sphinxcontrib-openapi": { + "hashes": [ + "sha256:1c1bd10d7653912c59a42f727c62cbb7b75f7905ddd9ccc477ebfd1bc69f0cf3", + "sha256:3c2a33c1d24733e6f22a0091b8172d08c723a7f192699f0dbfac99bb380dcb31" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, "sphinxcontrib-qthelp": { "hashes": [ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", @@ -1451,11 +1686,11 @@ }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", - "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" + "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", + "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" ], "markers": "python_version >= '3.5'", - "version": "==1.1.4" + "version": "==1.1.5" }, "toml": { "hashes": [ @@ -1510,6 +1745,20 @@ "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.4.3" }, + "types-python-dateutil": { + "hashes": [ + "sha256:39bfe0bde61fc673b8fa28167bd78622d976210f791971b9f3e10877cbf119a4", + "sha256:e6486ca27b6dde73e0ec079a9e1b03e208766e6bc7f1e08964a7e9104a5c7d7a" + ], + "version": "==0.1.4" + }, + "types-setuptools": { + "hashes": [ + "sha256:71ed0f4c71d8fb5f3026a90ae82d163c13749b110e157d82126725ac8f714360", + "sha256:b3ada82b21dcb8e0cafd7658d8a16018a000e55bdb7f6f3cec033223360563ce" + ], + "version": "==57.0.0" + }, "typing-extensions": { "hashes": [ "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", @@ -1521,19 +1770,19 @@ }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.4" + "version": "==1.26.6" }, "webargs": { "hashes": [ - "sha256:2f3d883ce9f348fa884889440fcc1b207e7c67b04be5e90be00a5be73e2cd91d", - "sha256:ce8c565789ece1584be7edba41617319eb75de59b2987958bee3f3fdb3669e60" + "sha256:565666bafd08dd375fae995a9d4187604741efc27b8ec7c5b5bcb0ae90b2c677", + "sha256:7b06eebf23d05ebec1aa97e9c373deadaa8194866fae79c56e0453804db7a756" ], "markers": "python_version >= '3.6'", - "version": "==7.0.1" + "version": "==8.0.0" }, "werkzeug": { "hashes": [ @@ -1546,6 +1795,7 @@ "wrapt": { "hashes": [ "sha256:1ba3a6bcb5014ed4d9c98afe8aa47b5ed320f84292b4f1b9f6afa0f4b2b91351", + "sha256:548a5381002654b72c8b8d1e167722e2a11169535d017d79d9cdee76502c1bc6", "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7", "sha256:c16fd7cad3d27a5db392ae0e6215533a912a7d3d83a40b8cf275744276989780", "sha256:d3294c918c5529e50b5454d39b589648d9baf6152e380067606d5566d71369a0" @@ -1554,18 +1804,19 @@ }, "zeroconf": { "hashes": [ - "sha256:3608be2db58f6f0dc70665e02ab420fb8bf428016f2c78403d879e066ecc9bff", - "sha256:4be24a10aa9c73406f48d42a8b3b077c217b0e8d7ed1e57639630da520c25959" + "sha256:46992d0786129c769c652c875221d394a63bdbef12832d443ac9e521005d46e1", + "sha256:ac88738639669f924859a4e01d9742f3783e619e710103f4a0d321a47f260fd2", + "sha256:de14268be7a449691f74463cce5ecbeeb7e5f572878a28285b11236b0171d407" ], - "version": "==0.28.8" + "version": "==0.33.1" }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" ], "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "version": "==3.5.0" } } } diff --git a/docs/Makefile b/docs/Makefile index 5d25534e..d2829e5b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -13,7 +13,12 @@ help: .PHONY: help Makefile +openapi: $(BUILDDIR)/swagger.yaml + +$(BUILDDIR)/swagger.yaml: Makefile + ofm-generate-openapi -o $@ + # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile +html dirhtml singlehtml json htmlhelp epub latex latexpdf text changes xml linkcheck doctest coverage clean: openapi Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst index 4fd4b616..f33d32a0 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -6,4 +6,10 @@ Live documentation Full, interactive Swagger documentation for your microscopes web API is available from the microscope itself. From any browser, go to ``http://{your microscope IP address}/api/v2/docs/swagger-ui``. -.. note:: We should have an online copy of the API SwaggerUI documentation up soon. \ No newline at end of file +.. note:: We should have an online copy of the API SwaggerUI documentation up soon. + +The API is now also detailed below, or there is a more interactive version of the docs statically compiled using ReDoc_ + +.. openapi:: ../build/swagger.yaml + +.. _ReDoc: api_redoc.html \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 4fd4b0c0..e3550325 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,12 +15,15 @@ import os import sys -# Load module from relative imports +# Load module from relative imports by modifying the path module_path = os.path.abspath("../..") sys.path.insert(0, module_path) # Handle mock imports for non-platform-agnostic modules +# This allows modules to load that depend on hardware that's not present, e.g. +# the `picamera` related modules. They won't *work* but if they load, we can +# extract the docstrings, which is what we care about here. from unittest.mock import MagicMock @@ -41,6 +44,7 @@ project = "OpenFlexure Microscope Software" copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin author = "Bath Open Instrumentation Group" +# TODO: extract version from ../setup.py # The short X.Y version version = "" # The full version, including alpha/beta/rc tags @@ -64,9 +68,8 @@ extensions = [ "sphinx.ext.viewcode", "sphinx.ext.githubpages", "sphinx.ext.ifconfig", - "sphinxcontrib.httpdomain", - "sphinxcontrib.autohttp.flask", - "sphinxcontrib.autohttp.flaskqref", + "sphinxcontrib.openapi", + "sphinxcontrib.redoc", ] # Override ordering @@ -222,6 +225,7 @@ epub_exclude_files = ["search.html"] # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. +# TODO: update with pysangaboard? intersphinx_mapping = { "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), "picamerax": ("https://picamerax.readthedocs.io/en/latest//", None), @@ -233,3 +237,13 @@ intersphinx_mapping = { # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True + +# -- Options for redoc extension --------------------------------------------- +redoc = [ + { + "name": "OpenFlexure Microscope HTTP API", + "page": "api_redoc", + "spec": "../build/swagger.yaml", + "embed": True, + } +] diff --git a/docs/source/extensions/actions.rst b/docs/source/extensions/actions.rst index 5463ccfd..1a9fb2ac 100644 --- a/docs/source/extensions/actions.rst +++ b/docs/source/extensions/actions.rst @@ -22,7 +22,9 @@ Like properties, we use a special view class to identify a view as an action: `` # Our success response (200) returns an image (image/jpeg mimetype) responses = { - 200: {"content_type": "image/jpeg"} + 200: { + "content": { "image/jpeg": {} }, + } } def post(self, args): diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py index 0ac21880..ca2578db 100644 --- a/docs/source/extensions/example_extension/05_actions.py +++ b/docs/source/extensions/example_extension/05_actions.py @@ -94,7 +94,7 @@ class QuickCaptureAPI(ActionView): args = {"use_video_port": fields.Boolean(missing=True)} # Our success response (200) returns an image (image/jpeg mimetype) - responses = {200: {"content_type": "image/jpeg"}} + responses = {200: {"content": {"image/jpeg": {}}}} def post(self, args): """ diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index d288bf65..444fc7fc 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import argparse import atexit import logging import logging.handlers @@ -23,7 +24,7 @@ import os from datetime import datetime import pkg_resources -from flask import abort, send_file, jsonify +from flask import abort, jsonify, send_file from flask_cors import CORS, cross_origin from labthings import create_app from labthings.extensions import find_extensions @@ -39,6 +40,8 @@ from openflexure_microscope.paths import ( logs_file_path, ) +from .openapi import add_spec_extras + # Custom RotatingFileHandler subclass class CustomRotatingFileHandler(logging.handlers.RotatingFileHandler): @@ -207,6 +210,9 @@ def api_v1_catch_all(path): # pylint: disable=W0613 abort(410, "API v1 is no longer in use. Please upgrade your client.") +add_spec_extras(labthing.spec) + + # Automatically clean up microscope at exit def cleanup(): logging.debug("App teardown started...") @@ -242,6 +248,39 @@ def ofm_serve(): server.run(host="0.0.0.0", port=5000, debug=debug_app, zeroconf=True) +def generate_openapi(): + parser = argparse.ArgumentParser("Generate an OpenAPI specification document") + parser.add_argument( + "-o", + dest="output", + default="openapi.yaml", + help=( + "Specify the output filename. If it ends in .json, we output JSON." + "Use .yml or .yaml for YAML (which is the default" + ), + ) + parser.add_argument( + "--validate", + action="store_true", + help="Validate the API spec, returning an error code if it does not pass.", + ) + args = parser.parse_args() + if args.validate: + import apispec.utils + + if apispec.utils.validate_spec(labthing.spec): + print("OpenAPI specification validated OK.") + fname = args.output + if fname.endswith(".json"): + import json + + with open(fname, "w") as fd: + json.dump(labthing.spec.to_dict(), fd) + else: + with open(fname, "w") as fd: + fd.write(labthing.spec.to_yaml()) + + # Start the app if the module is run directly if __name__ == "__main__": ofm_serve() diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index 15044f70..5f736121 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -1,11 +1,13 @@ +import inspect import logging import time from contextlib import contextmanager -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple, cast import numpy as np from labthings import current_action, fields, find_component from labthings.extensions import BaseExtension +from labthings.utilities import get_docstring, get_summary from labthings.views import ActionView, View from scipy import ndimage @@ -19,6 +21,10 @@ from openflexure_microscope.utilities import set_properties class JPEGSharpnessMonitor: + """Monitor JPEG frame size in a background thread + + This class starts a background thread """ + def __init__(self, microscope: Microscope): self.microscope: Microscope = microscope self.camera: BaseCamera = microscope.camera @@ -144,6 +150,124 @@ def sharpness_edge(image: np.ndarray) -> float: ) +def find_microscope() -> Microscope: + """Find the microscope component or raise an exception. + + This function will fail with HTTPError extensions if it can't + find the appropriate hardware. + + We return a `Microscope` object + """ + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + return microscope + + +def find_microscope_with_real_stage() -> Microscope: + """Find the microscope and ensure it has a real stage. + + This function wraps `find_microscope()` and additionally asserts + that there is a real stage, raising a `503` code if not. + """ + microscope = find_microscope() + + if not microscope.has_real_stage(): + abort(503, "No stage connected. Unable to autofocus.") + + return microscope + + +def extension_action(args=None): + """A decorator to auto-create an Action endpoint for a method + + Use this decorator on any method of an extension (`BaseExtension` subclass) + and it will automatically be added to the API. At present it is deliberately + basic, and the plan is to expand the options in the future. + + Currently, you may specify `args` (which is a Marshmallow-format schema + or dictionary, determining the datatype of the arguments, which will be + taken from the JSON payload of the POST request initiating the action). + + The parsed arguments dictionary is expanded as the function arguments, + i.e. we call `decorated_method(self, **kwargs)`. This means that any + unused arguments will cause an error, which is probably good practice... + + NB this decorator does **not** replace the function with a `View` or + register it with the parent `Extension`. It adds the created `View` as + a property of the function, `flask_view`. The parent `Extension` is + responsible for collating and adding the views in its `__init__` method. + """ + supplied_args = args + + def decorator(func): + class_docstring = f"""Manage actions for {func.__name__}. + + This `View` class will return a list of `Action` objects representing + each time {func.__name__} has been run in response to a `GET` request, + and will start a new `Action` in response to a `POST` request. + """ + + class ActionViewWrapper(ActionView): + __doc__ = class_docstring + args = supplied_args + + def post(self, arguments): + # Run the action + return func(self.extension, **arguments) + + def get(self, *args, **kwargs): # pylint: disable=useless-super-delegation + # Explicitly wrap the `get` method to allow us to add a docstring + return super().get(*args, **kwargs) + + # Create a nice docstring. NB because the source function and this docstring + # aren't guaranteed to have the same leading whitespace, we just make sure + # both docstrings are stripped of leading indent, using `inspect.cleandoc()` + ActionViewWrapper.post.description = ( + get_docstring(func, remove_newlines=False) + + "\n\n" + + inspect.cleandoc( + """ + This `POST` request starts an Action, i.e. the hardware will do something + that may continue after the HTTP request has been responded to. The + response will always be an Action object, that details the current + status of the action and provides an interface to poll for completion. + + If the action completes within a specified timeout, we will return + an HTTP status code of `200` and the return value will include any + output from the action. If it does not complete, we will return a + `201` response code, and the action's endpoint may be polled to follow + its progress. + """ + ) + ) + ActionViewWrapper.post.summary = get_summary(func) + ActionViewWrapper.post.__doc__ = ActionViewWrapper.post.description + ActionViewWrapper.__name__ = func.__name__ + ActionViewWrapper.get.summary = ( + f"List running and completed `{func.__name__}` actions." + ) + ActionViewWrapper.get.description = ( + ActionViewWrapper.get.summary + + "\n\n" + + inspect.cleandoc( + f""" + This `GET` request will return a list of `Action` objects corresponding + to the `{func.__name__}` action. It will include all the times it has + been run since the server was last restarted, including running, completed, + and failed attempts. + """ + ) + ) + + func.flask_view = ActionViewWrapper + return func + + return decorator + + ### Autofocus extension @@ -157,21 +281,34 @@ class AutofocusExtension(BaseExtension): self.add_view( MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness" ) - self.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus") - self.add_view(FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus") - self.add_view( - UpDownUpAutofocusAPI, "/updownup_autofocus", endpoint="updownup_autofocus" - ) + self.add_decorated_method_views() - self.add_view( - MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure" - ) + def add_decorated_method_views(self): + """Add views from any methods that have been decorated + + Using the decorators `@extension_action()` et al will add a + property to the decorated method, `method_view`. If this is + present, this function will add the views to the extension. + """ + for k in dir(self): + obj = getattr(self, k) + if hasattr(obj, "flask_view"): + name = obj.__name__ + self.add_view(obj.flask_view, f"/{name}", endpoint=name) def measure_sharpness( - self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2 + self, + microscope: Optional[Microscope] = None, + metric_fn: Callable = sharpness_sum_lap2, ) -> float: - """Measure the sharpness of the camera's current view.""" - + """Measure the sharpness from the MJPEG stream + + Take a JPEG snapshot from the camera (extracted from the live preview stream) + and return its size. This is the sharpness metric used by the fast autofocus + method. + """ + if not microscope: + microscope = find_microscope() if hasattr(microscope.camera, "array") and callable( getattr(microscope.camera, "array") ): @@ -179,22 +316,38 @@ class AutofocusExtension(BaseExtension): else: raise RuntimeError(f"Object {microscope.camera} has no method `array`") + @extension_action( + args={ + "dz": fields.List( + fields.Int(), + description="An ascending list of relative z positions", + example=[int(x) for x in np.linspace(-300, 300, 7)], + ) + } + ) def autofocus( self, - microscope: Microscope, - dz: List[int], + microscope: Optional[Microscope] = None, + dz: Optional[List[int]] = None, settle: float = 0.5, metric_fn: Callable = sharpness_sum_lap2, ) -> Tuple[List[int], List[float]]: """Perform a simple autofocus routine. + The stage is moved to z positions (relative to current position) in dz, and at each position an image is captured and the sharpness function evaulated. We then move back to the position where the sharpness was highest. No interpolation is performed. + dz is assumed to be in ascending order (starting at -ve values) """ + if not microscope: + microscope = find_microscope_with_real_stage() camera: BaseCamera = microscope.camera stage: BaseStage = microscope.stage + if not dz: + dz = list(np.linspace(-300, 300, 7)) + dz = cast(List[int], dz) # dz can't now be None, so fix its type. with set_properties(stage, backlash=256), stage.lock, camera.lock: sharpnesses: List[float] = [] @@ -216,25 +369,115 @@ class AutofocusExtension(BaseExtension): return positions, sharpnesses - def move_and_find_focus(self, microscope: Microscope, dz: int) -> int: + def move_and_find_focus( + self, microscope: Optional[Microscope] = None, dz: int = 0 + ) -> int: """Make a relative Z move and return the peak sharpness position""" + if not microscope: + microscope = find_microscope_with_real_stage() with monitor_sharpness(microscope) as m: m.focus_rel(dz) return m.sharpest_z_on_move(0) + @extension_action( + args={ + "dz": fields.Int(required=True, description="The relative Z move to make") + } + ) def move_and_measure( - self, microscope: Microscope, dz: int + self, microscope: Optional[Microscope] = None, dz: int = 0 ) -> Dict[str, np.ndarray]: - """Make a relative Z move and return the sharpness data""" + """Make a relative move in Z and measure dynamic sharpness + + This accesses the underlying method used by the fast autofocus routines, to + move the stage while monitoring the sharpness, as reported by the size of + each JPEG frame in the preview stream. It returns a dictionary with + stage position vs time and image size (i.e. sharpness) vs time. + """ + if not microscope: + microscope = find_microscope_with_real_stage() with monitor_sharpness(microscope) as m: m.focus_rel(dz) return m.data_dict() + @extension_action( + args={ + "dz": fields.Int( + missing=2000, + example=2000, + description="Total Z range to search over (in stage steps)", + ) + } + ) def fast_autofocus( - self, microscope: Microscope, dz: int = 2000 + self, microscope: Optional[Microscope] = None, dz: int = 2000 ) -> Dict[str, np.ndarray]: - """Perform a down-up-down-up autofocus""" - with microscope.camera.lock, microscope.stage.lock: + """Perform a fast down-up-down-up autofocus + + This "fast" autofocus method moves the stage continuously in Z, while + following the sharpness using the MJPEG stream. This version is the + simplest "fast" autofocus method, and performs the following sequence + of moves: + + 1. Move to `-dz/2`, i.e. the bottom of the range + 2. Move up by `dz`, i.e. to the top of the range, while recording the + sharpness of the image as a function of time. Record the estimated + position of the stage when the sharpness was maximised, `fz`. + 3. Move back to the bottom (by `-dz`) + 4. Move up to the position where it was sharpest. + + ## Backlash correction + This routine should cancel out backlash: the stage is moving upwards as + we record the sharpnes vs z data, and it is also moving upwards when + we make the final move to the sharpest point. Mechanical backlash should + therefore be the same in both cases. + + This does not account for lag between the sharpness measurements and the + stage's motion; that has been tested for and seems not to be a big issue + most of the time, but may need to be accounted for in the future, if + hardware or software changes increase the latency. + + ## Sharpness metric + This method uses the MJPEG preview stream to estimate the sharpness of + the image. MJPEG streams consist of a series of independent JPEG images, + so each frame can be looked at in isolation (though see later for an + important caveat). JPEG images are compressed lossily, by taking the + discrete cosine transform (DCT) of each 8x8 block in the image. A very + rough precis of how this works is that after the DCT, cosine components + that are deemed unimportant (i.e. smaller than a threshold) are discarded. + The effect is that images with lots of high-frequency information have a + larger file size. + + We look only at the size of each JPEG frame in the stream, so we get a + remarkably robust estimate of image sharpness without even opening the + images! That's what lets us analyse 30 images/second even on the very + limited processing power available to the Raspberry Pi 3. + + ## Warning: frame independence + We assume that JPEG frames are independent. This is only true if the + MJPEG stream is encoded at *constant quality* without any additional + bit rate control. By default, many streams will reduce the quality + factor if they exceed a target bit rate, which badly affects this + method. We turn off bit rate limiting for the Raspberry Pi camera, + which fixes the problem, at the expense of sometimes failing if + particularly sharp images appear in the stream, as there is a fairly + small maximum size for each JPEG frame beyond which empty images are + returned. + + ## Estimation of sharpness vs z + What we record during an autofocus is two time series, from two parallel + threads. One thread monitors the camera, and records the size of each + JPEG frame as a function of time. NB this is time from `time.time()` + in Python, so will not be microsecond-accurate. The other thread is + responsible for moving the stage, and records its current Z position + before and after each move. Interpolating between these `(t, z)` points + gives us a `z` value for each JPEG size, and so we can estimate the + JPEG size as a function of `z` and hence determine the `z` value at + which sharpness is maximised. + """ + if not microscope: + microscope = find_microscope_with_real_stage() + with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock: with monitor_sharpness(microscope) as m: # Move to (-dz / 2) m.focus_rel(-dz / 2) @@ -253,15 +496,44 @@ class AutofocusExtension(BaseExtension): # Return all focus data return m.data_dict() + @extension_action( + args={ + "dz": fields.Int( + missing=2000, + example=2000, + description="Total Z range to search over (in stage steps)", + ), + "target_z": fields.Int( + missing=0, + example=-100, + description="Target finishing position, relative to the focus.", + ), + "initial_move_up": fields.Bool( + missing=True, + description="Set to Flase to disable the initial move upwards", + ), + "backlash": fields.Int( + missing=25, + minimum=0, + description="Distance to undershoot, before correction move.", + ), + } + ) def fast_up_down_up_autofocus( self, - microscope: Microscope, + microscope: Optional[Microscope] = None, dz: int = 2000, target_z: int = 0, initial_move_up: bool = True, - mini_backlash: int = 25, + backlash: Optional[int] = None, + mini_backlash: Optional[int] = None, ) -> Dict[str, np.ndarray]: - """Autofocus by measuring on the way down, and moving back up with feedback. + """Perform a fast up-down-up autofocus, with feedback + + Autofocus by measuring on the way down, and moving back up with feedback. + This is a "fast" autofocus method, i.e. it moves the stage continuously + while monitoring the sharpness using the MJPEG stream. See `fast_autofocus` + for more details. This autofocus method is very efficient, as it only passes the peak once. The sequence of moves it performs is: @@ -277,26 +549,36 @@ class AutofocusExtension(BaseExtension): target position. Moving back to the target position in two steps allows us to correct for - backlash, by using the sharpness-vs-z curve as a rough encoder for Z. + backlash, by using the sharpness-vs-z curve as a rough encoder for Z. The + main source of error is that the curves on the way up and the way down are + not always identical, largely due to small lateral shifts as the Z axis is + moved. Parameters: - dz: number of steps over which to scan (optional, default 2000) + * `dz`: number of steps over which to scan (optional, default 2000) - target_z: we aim to finish at this position, relative to focus. This may - be useful if, for example, you want to acquire a stack of images in Z. - It is optional, and the default value of 0 will finish at the focus. + * `target_z`: we aim to finish at this position, relative to focus. This may + be useful if, for example, you want to acquire a stack of images in Z. + It is optional, and the default value of 0 will finish at the focus. - initial_move_up: (optional, default True) set this to `False` to move down - from the starting position. Mostly useful if you're able to combine - the initial move with something else, e.g. moving to the next scan point. + * `initial_move_up`: (optional, default True) set this to `False` to move down + from the starting position. Mostly useful if you're able to combine + the initial move with something else, e.g. moving to the next scan point. - mini_backlash: (optional, default 25) is a small extra move made in step - 3 to help counteract backlash. It should be small enough that you - would always expect there to be greater backlash than this. Too small - might slightly hurt accuracy, but is unlikely to be a big issue. Too big - may cause you to overshoot, which is a problem. + * **backlash**: (optional, default 25) is a small extra move made in step + 3 to help counteract backlash. It should be small enough that you + would always expect there to be greater backlash than this. Too small + might slightly hurt accuracy, but is unlikely to be a big issue. Too big + may cause you to overshoot, which is a problem. + + * **mini_backlash**: (optional, default 25) is an alias for `backlash` and will be + removed in due course. """ - with microscope.camera.lock, microscope.stage.lock: + if not microscope: + microscope = find_microscope_with_real_stage() + if not mini_backlash: # I renamed the argument, + mini_backlash = backlash or 25 + with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock: with monitor_sharpness(microscope) as m: # Ensure the MJPEG stream has started microscope.camera.start_stream() @@ -347,116 +629,7 @@ class AutofocusExtension(BaseExtension): class MeasureSharpnessAPI(View): + __doc__ = AutofocusExtension.measure_sharpness.__doc__ + def post(self): - microscope = find_component("org.openflexure.microscope") - - if not microscope: - abort(503, "No microscope connected. Unable to measure sharpness.") - - return {"sharpness": self.extension.measure_sharpness(microscope)} - - -class MoveAndMeasureAPI(ActionView): - """ - Move the stage and measure dynamic sharpness - """ - - args = {"dz": fields.Int(required=True)} - - def post(self, args): - microscope = find_component("org.openflexure.microscope") - - if not microscope: - abort(503, "No microscope connected. Unable to autofocus.") - - if microscope.has_real_stage(): - # Acquire microscope lock with 1s timeout - with microscope.camera.lock, microscope.stage.lock: - return self.extension.move_and_measure(microscope, dz=args.get("dz")) - - else: - abort(503, "No stage connected. Unable to autofocus.") - - -class AutofocusAPI(ActionView): - """ - Run a standard autofocus - """ - - args = {"dz": fields.List(fields.Int())} - - def post(self, args): - microscope = find_component("org.openflexure.microscope") - - if not microscope: - abort(503, "No microscope connected. Unable to autofocus.") - - # Figure out the range of z values to use - dz = np.array(args.get("dz", np.linspace(-300, 300, 7))) - - if microscope.has_real_stage(): - logging.debug("Running autofocus...") - - # return a handle on the autofocus task - return self.extension.autofocus(microscope, dz) - - else: - abort(503, "No stage connected. Unable to autofocus.") - - -class FastAutofocusAPI(ActionView): - """ - Run a fast autofocus - """ - - args = {"dz": fields.Int(missing=2000, example=2000)} - - def post(self, args): - microscope = find_component("org.openflexure.microscope") - - if not microscope: - abort(503, "No microscope connected. Unable to autofocus.") - - if microscope.has_real_stage(): - logging.debug("Running autofocus...") - # Acquire microscope lock with 1s timeout - with microscope.lock(timeout=1): - # Run fast_autofocus - return self.extension.fast_autofocus(microscope, dz=args.get("dz")) - - else: - abort(503, "No stage connected. Unable to autofocus.") - - -class UpDownUpAutofocusAPI(ActionView): - """ - Run a fast up-down-up autofocus - """ - - args = { - "dz": fields.Int(missing=2000, example=2000), - "backlash": fields.Int(missing=25, minimum=0), - } - - def post(self, args): - microscope = find_component("org.openflexure.microscope") - - if not microscope: - abort(503, "No microscope connected. Unable to autofocus.") - - # Figure out the parameters to use - dz = args.get("dz") - backlash = args.get("backlash") - - if microscope.has_real_stage(): - logging.debug("Running autofocus...") - - # Acquire microscope lock with 1s timeout - with microscope.lock(timeout=1): - # Run fast_up_down_up_autofocus - return self.extension.fast_up_down_up_autofocus( - microscope, dz=dz, mini_backlash=backlash - ) - - else: - abort(503, "No stage connected. Unable to autofocus.") + return {"sharpness": self.extension.measure_sharpness()} diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py index 782ad8f4..3bb502a1 100644 --- a/openflexure_microscope/api/default_extensions/scan.py +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -2,10 +2,10 @@ import datetime import logging import time import uuid -import marshmallow from functools import reduce from typing import Dict, List, Optional, Tuple +import marshmallow from labthings import ( current_action, fields, diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py index 74444a07..077988fc 100644 --- a/openflexure_microscope/api/default_extensions/zip_builder.py +++ b/openflexure_microscope/api/default_extensions/zip_builder.py @@ -10,7 +10,7 @@ from labthings import fields, find_component, update_action_progress from labthings.extensions import BaseExtension from labthings.schema import Schema, pre_dump from labthings.utilities import description_from_view -from labthings.views import ActionView, PropertyView, View +from labthings.views import ActionView, PropertyView, View, described_operation from openflexure_microscope.captures import CaptureObject from openflexure_microscope.microscope import Microscope @@ -149,15 +149,24 @@ class ZipBuilderAPIView(ActionView): args = fields.List(fields.String(), required=True) def post(self, args): + """Build a zip file of some captures + + Given a list of capture IDs as its argument, this action will + create a zip file that can be downloaded once the action has + completed. + """ microscope = find_component("org.openflexure.microscope") - # Return a handle on the autofocus task + # Build a zip file from the supplied IDs return self.extension.manager.marshaled_build_zip_from_capture_ids( microscope, args ) class ZipListAPIView(PropertyView): + """List all the zip files currently available for download. + """ + schema = ZipObjectSchema(many=True) def get(self): @@ -165,10 +174,21 @@ class ZipListAPIView(PropertyView): class ZipGetterAPIView(View): - """ - Download or delete a particular capture collection ZIP file + """Download or delete a particular capture collection ZIP file """ + parameters = [ + { + "name": "session_id", + "in": "path", + "description": "The unique ID of the zip builder session", + "required": True, + "schema": {"type": "string"}, + } + ] + responses = {404: {"description": "The session ID could not be found"}} + + @described_operation def get(self, session_id): """ Download a particular capture collection ZIP file @@ -176,7 +196,7 @@ class ZipGetterAPIView(View): if not session_id in self.extension.manager.session_zips: return abort(404) # 404 Not Found - logging.info("Session ID: %s", session_id) + logging.info("Retrieving zip (session ID: %s)", session_id) return send_file( self.extension.manager.zip_fp_from_id(session_id).name, @@ -185,6 +205,14 @@ class ZipGetterAPIView(View): attachment_filename=f"{session_id}.zip", ) + get.responses = { + 200: { + "content": {"application/zip": {}}, + "description": "A zip archive containing the selected captures", + } + } + + @described_operation def delete(self, session_id): """ Close and delete a particular capture collection ZIP file @@ -198,3 +226,5 @@ class ZipGetterAPIView(View): del self.extension.manager.session_zips[session_id] return {"return": session_id} + + delete.responses = {200: {"description": "The zip file was deleted"}} diff --git a/openflexure_microscope/api/openapi.py b/openflexure_microscope/api/openapi.py new file mode 100644 index 00000000..f0605954 --- /dev/null +++ b/openflexure_microscope/api/openapi.py @@ -0,0 +1,36 @@ +API_TAGS = [ + { + "name": "actions", + "description": ( + "Actions that can be run on the microscope. These endpoints represent " + "actions that many not complete immediately, so they run on the server " + "as `Action` objects and their status can be queried using the links " + "embedded in the JSON action description." + ), + "externalDocs": { + "url": "https://iot.mozilla.org/wot/#action-resource", + "description": "Mozilla's description of Web of Things 'Action' resources.", + }, + }, + { + "name": "properties", + "description": ( + "Properties can be read and/or written to, and affect the " + "state of the microscope." + ), + "externalDocs": { + "url": "https://iot.mozilla.org/wot/#property-resource", + "description": "Mozilla's description of Web of Things 'Property' resources.", + }, + }, + {"name": "captures", "description": ""}, + {"name": "extensions", "description": ""}, + {"name": "events", "description": ""}, +] + + +def add_spec_extras(spec): + """Add extra documentation and features to the OpenAPI spec""" + # Add a list of tags, so we can control ordering and add descriptions + for t in API_TAGS: + spec.tag(t) diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py index b5dab436..a6ef3b12 100644 --- a/openflexure_microscope/api/v2/views/actions/camera.py +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -66,12 +66,15 @@ class CaptureAPI(ActionView): class RAMCaptureAPI(ActionView): - """ - Take a non-persistant image capture. - """ + """Take a non-persistent image capture.""" args = BasicCaptureArgs() - responses = {200: {"content_type": "image/jpeg"}} + responses = { + 200: { + "content": {"image/jpeg": {}}, + "description": "A JPEG image, representing the capture", + } + } def post(self, args): """ diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py index 206d9623..dd87e7e0 100644 --- a/openflexure_microscope/api/v2/views/actions/stage.py +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -9,16 +9,19 @@ class MoveStageAPI(ActionView): "absolute": fields.Boolean( missing=False, example=False, description="Move to an absolute position" ), - "x": fields.Int(missing=None, example=100), - "y": fields.Int(missing=None, example=100), - "z": fields.Int(missing=None, example=20), + "x": fields.Int(missing=None, example=100, allow_none=False), + "y": fields.Int(missing=None, example=100, allow_none=False), + "z": fields.Int(missing=None, example=20, allow_none=False), } def post(self, args): """ Move the microscope stage in x, y, z - Any axes that are not specifed will not move. + This action moves the stage. Any axes that are not specifed will not move. + If `absolute=True` is specified, the stage will move to the absolute + coordinates given. If not (the default), a relative move is made, i.e. + `x=0, y=0, z=0` corresponds to no motion. """ microscope = find_component("org.openflexure.microscope") @@ -50,7 +53,8 @@ class ZeroStageAPI(ActionView): def post(self): """ Zero the stage coordinates. - Does not move the stage, but rather makes the current position read as [0, 0, 0] + + This action does not move the stage, but rather makes the current position read as [0, 0, 0] """ microscope = find_component("org.openflexure.microscope") diff --git a/openflexure_microscope/api/v2/views/camera.py b/openflexure_microscope/api/v2/views/camera.py index 5b96b0f8..9179ed10 100644 --- a/openflexure_microscope/api/v2/views/camera.py +++ b/openflexure_microscope/api/v2/views/camera.py @@ -14,14 +14,14 @@ class LSTImageProperty(PropertyView): responses = { 200: { - "content_type": "image/png", + "content": {"image/jpeg": {}}, "description": "Lens-shading table in RGB format", } } def get(self): """ - Get the stage geometry. + Get the lens shading table as an image. """ microscope = find_component("org.openflexure.microscope") diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py index e3c1fce4..44f056ca 100644 --- a/openflexure_microscope/api/v2/views/captures.py +++ b/openflexure_microscope/api/v2/views/captures.py @@ -146,8 +146,19 @@ class CaptureList(PropertyView): return image_list +CAPTURE_ID_PARAMETER = { + "name": "id_", + "in": "path", + "description": "The unique ID of the capture", + "required": True, + "schema": {"type": "string"}, + "example": "eeae7ae9-0c0d-45a4-9ef2-7b84bb67a1d1", +} + + class CaptureView(View): tags = ["captures"] + parameters = [CAPTURE_ID_PARAMETER] @marshal_with(FullCaptureSchema()) def get(self, id_): @@ -162,6 +173,8 @@ class CaptureView(View): return capture_obj + get.responses = {404: {"description": "Capture object was not found"}} + def delete(self, id_): """ Delete a single image capture @@ -182,7 +195,20 @@ class CaptureView(View): class CaptureDownload(View): tags = ["captures"] - responses = {200: {"content_type": "image/jpeg"}} + responses = { + 200: {"content": {"image/jpeg": {}}, "description": "Image data in JPEG format"} + } + parameters = [ + CAPTURE_ID_PARAMETER, + { + "name": "filename", + "in": "path", + "description": "The filename of the downloaded image.", + "required": False, + "schema": {"type": "string"}, + "example": "myimage.jpeg", + }, + ] def get(self, id_, filename: Optional[str]): """ @@ -223,6 +249,7 @@ class CaptureDownload(View): class CaptureTags(View): tags = ["captures"] + parameters = [CAPTURE_ID_PARAMETER] def get(self, id_): """ @@ -270,6 +297,7 @@ class CaptureTags(View): class CaptureAnnotations(View): tags = ["captures"] + parameters = [CAPTURE_ID_PARAMETER] def get(self, id_): """ diff --git a/openflexure_microscope/api/v2/views/instrument.py b/openflexure_microscope/api/v2/views/instrument.py index 28d9ea8a..8bb2e37d 100644 --- a/openflexure_microscope/api/v2/views/instrument.py +++ b/openflexure_microscope/api/v2/views/instrument.py @@ -32,9 +32,24 @@ class SettingsProperty(PropertyView): return self.get() +ROUTE_PARAMETER = { + "name": "route", + "in": "path", + "description": ( + "The location of a key or sub-dictionary. " + + "This is formatted like a path, i.e. forward " + + "slashes delimit components of the path." + ), + "required": True, + "schema": {"type": "string"}, + "example": "camera/exposure_time", +} + + class NestedSettingsProperty(View): tags = ["properties"] responses = {404: {"description": "Settings key cannot be found"}} + parameters = [ROUTE_PARAMETER] def get(self, route: str): """ @@ -79,6 +94,7 @@ class StateProperty(PropertyView): class NestedStateProperty(View): tags = ["properties"] responses = {404: {"description": "Status key cannot be found"}} + parameters = [ROUTE_PARAMETER] def get(self, route): """ @@ -107,6 +123,7 @@ class ConfigurationProperty(PropertyView): class NestedConfigurationProperty(View): tags = ["properties"] responses = {404: {"description": "Status key cannot be found"}} + parameters = [ROUTE_PARAMETER] def get(self, route): """ diff --git a/openflexure_microscope/api/v2/views/stage.py b/openflexure_microscope/api/v2/views/stage.py index e65138eb..21d19e94 100644 --- a/openflexure_microscope/api/v2/views/stage.py +++ b/openflexure_microscope/api/v2/views/stage.py @@ -1,5 +1,6 @@ from labthings import fields, find_component from labthings.views import PropertyView +from marshmallow import validate class StageTypeProperty(PropertyView): @@ -8,8 +9,9 @@ class StageTypeProperty(PropertyView): schema = fields.String( missing=None, example="SangaStage", - OneOf=["SangaStage", "SangaDeltaStage"], + validate=validate.OneOf(["SangaStage", "SangaDeltaStage"]), description="The translation stage geometry", + allow_none=False, ) def get(self): diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py index 38a781c2..4f5ba9ff 100644 --- a/openflexure_microscope/api/v2/views/streams.py +++ b/openflexure_microscope/api/v2/views/streams.py @@ -17,7 +17,23 @@ class MjpegStream(PropertyView): Real-time MJPEG stream from the microscope camera """ - responses = {200: {"content_type": "multipart/x-mixed-replace"}} + responses = { + 200: { + "content": {"multipart/x-mixed-replace": {}}, + "description": ( + "An MJPEG stream of camera images.\n\n" + "This endpoint will serve JPEG images sequentially, \n" + "with each frame separated by `--frame` and a \n" + "`Content-Type: image/jpeg` header.\n" + "Using this endpoint as the `src` of an HTML `` \n" + "tag will result in the video stream displaying without \n" + "further effort.\n\n" + "If you save the stream to disk (e.g. with `curl`), be \n" + "aware that the text in between frames may confuse some \n" + "video players." + ), + } + } def get(self): """ @@ -44,7 +60,7 @@ class SnapshotStream(PropertyView): Single JPEG snapshot from the camera stream """ - responses = {200: {"content_type": "image/jpeg", "description": "Snapshot taken"}} + responses = {200: {"content": {"image/jpeg": {}}, "description": "Snapshot taken"}} def get(self): """ diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index da546b60..f97d3dc6 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -19,8 +19,8 @@ from openflexure_microscope.stage.sanga import SangaDeltaStage, SangaStage try: from openflexure_microscope.camera.pi import PiCameraStreamer -except Exception as e: # pylint: disable=W0703 - logging.error(e) +except Exception as exc: # pylint: disable=W0703 + logging.error(exc) logging.warning("Unable to import PiCameraStreamer") from labthings import CompositeLock diff --git a/pyproject.toml b/pyproject.toml index 1465fb29..27543614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ location = ".venv" [tool.poe.tasks] black = "black ." black_check = "black --check ." -isort = "isort ." +isort = "isort openflexure_microscope" pylint = "pylint openflexure_microscope" mypy = "mypy --cobertura-xml-report openflexure_microscope openflexure_microscope" test = "pytest ." diff --git a/setup.py b/setup.py index b6f552d8..036a7b97 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ setup( # set up the project for development, you use those specific packages, rather than # the looser specifications given here. install_requires=[ + "apispec[validation]", # We need the extra to validate the spec "Flask ~= 1.0", "Pillow ~= 7.2.0", "numpy ~= 1.20", @@ -58,7 +59,7 @@ setup( "pyyaml ~= 5.4.0", "pytest-cov ~= 2.10.1", "piexif ~= 1.1.3", - "labthings ~= 1.2.2", + "labthings ~= 1.3.0", "typing-extensions ~= 3.7.4", # Needed for some type-hints in Python < 3.8 (e.g. Literal) "RPi.GPIO ~= 0.7.0; platform_machine == 'armv7l'", ], @@ -67,11 +68,15 @@ setup( # them to specific versions to enable consistent builds and testing. extras_require={ "dev": [ - "sphinxcontrib-httpdomain ~= 1.7", + "sphinx < 4.0", # Currently httpdomain isn't ready for 4.0 + "sphinxcontrib-openapi ~= 0.7", + "sphinx_rtd_theme ~=0.5.2", "rope ~= 0.14.0", - "pylint ~= 2.3", + "pylint ~= 2.8.0", # 2.9.2 crashes and I've not yet figured out why. "pytest ~= 6.1.2", "mypy ~= 0.790", + "types-python-dateutil", + "types-setuptools", "poethepoet ~= 0.10.0", "freezegun ~= 1.0.0", "lxml ~= 4.6", @@ -87,6 +92,7 @@ setup( "console_scripts": [ "ofm-serve=openflexure_microscope.api.app:ofm_serve", "ofm-rescue=openflexure_microscope.rescue.auto:main", + "ofm-generate-openapi=openflexure_microscope.api.app:generate_openapi", ] }, project_urls={ diff --git a/tests/test_app.py b/tests/test_app.py index c1ef2d0e..0e16c58b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,13 @@ +from apispec.utils import validate_spec +import json +import jsonschema +import os + from openflexure_microscope.api.app import api_microscope, app, labthing from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.microscope import Microscope from openflexure_microscope.stage.base import BaseStage +from labthings.json import encode_json def test_app_creation(): @@ -12,3 +18,26 @@ def test_microscope_creation(): assert isinstance(api_microscope, Microscope) assert isinstance(api_microscope.camera, BaseCamera) assert isinstance(api_microscope.stage, BaseStage) + + +def test_openapi_valid(): + assert validate_spec(labthing.spec) + + +def test_thing_description_valid(): + # TODO: it would be nice to put this into LabThings + # First load the schema from file and check it validates + schema_fname = os.path.join(os.path.dirname(__file__), "w3c_td_schema.json") + schema = json.load(open(schema_fname, "r")) + jsonschema.Draft7Validator.check_schema(schema) + + # Build a TD dictionary + with labthing.app.test_request_context(): + td_dict = labthing.thing_description.to_dict() + + # Allow our LabThingsJSONEncoder to encode the RD + td_json = encode_json(td_dict) + # Decode the JSON back into a primitive dictionary + td_json_dict = json.loads(td_json) + # Validate + jsonschema.validate(instance=td_json_dict, schema=schema) diff --git a/tests/w3c_td_schema.json b/tests/w3c_td_schema.json new file mode 100644 index 00000000..62194083 --- /dev/null +++ b/tests/w3c_td_schema.json @@ -0,0 +1,1035 @@ +{ + "title": "WoT TD Schema - 16 October 2019", + "description": "JSON Schema for validating TD instances against the TD model. TD instances can be with or without terms that have default values", + "$schema ": "http://json-schema.org/draft-07/schema#", + "definitions": { + "anyUri": { + "type": "string", + "format": "iri-reference" + }, + "description": { + "type": "string" + }, + "descriptions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "title": { + "type": "string" + }, + "titles": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "security": { + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "scopes": { + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "subprotocol": { + "type": "string", + "enum": [ + "longpoll", + "websub", + "sse" + ] + }, + "thing-context-w3c-uri": { + "type": "string", + "enum": [ + "https://www.w3.org/2019/wot/td/v1" + ] + }, + "thing-context": { + "oneOf": [{ + "type": "array", + "items": [{ + "$ref": "#/definitions/thing-context-w3c-uri" + }], + "additionalItems": { + "anyOf": [{ + "$ref": "#/definitions/anyUri" + }, + { + "type": "object" + } + ] + } + }, + { + "$ref": "#/definitions/thing-context-w3c-uri" + } + ] + }, + "type_declaration": { + "oneOf": [{ + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "dataSchema": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "title": { + "$ref": "#/definitions/title" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "writeOnly": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + }, + "unit": { + "type": "string" + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "format": { + "type": "string" + }, + "const": {}, + "type": { + "type": "string", + "enum": [ + "boolean", + "integer", + "number", + "string", + "object", + "array", + "null" + ] + }, + "items": { + "oneOf": [{ + "$ref": "#/definitions/dataSchema" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + } + ] + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "properties": { + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "form_element_property": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "readproperty", + "writeproperty", + "observeproperty", + "unobserveproperty" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "readproperty", + "writeproperty", + "observeproperty", + "unobserveproperty" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "form_element_action": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "invokeaction" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "invokeaction" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "form_element_event": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "subscribeevent", + "unsubscribeevent" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "subscribeevent", + "unsubscribeevent" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "form_element_root": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "readallproperties", + "writeallproperties", + "readmultipleproperties", + "writemultipleproperties" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "readallproperties", + "writeallproperties", + "readmultipleproperties", + "writemultipleproperties" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "property_element": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_property" + } + }, + "uriVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "observable": { + "type": "boolean" + }, + "writeOnly": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + }, + "unit": { + "type": "string" + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "format": { + "type": "string" + }, + "const": {}, + "type": { + "type": "string", + "enum": [ + "boolean", + "integer", + "number", + "string", + "object", + "array", + "null" + ] + }, + "items": { + "oneOf": [{ + "$ref": "#/definitions/dataSchema" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + } + ] + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "properties": { + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "forms" + ], + "additionalProperties": true + }, + "action_element": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_action" + } + }, + "uriVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "input": { + "$ref": "#/definitions/dataSchema" + }, + "output": { + "$ref": "#/definitions/dataSchema" + }, + "safe": { + "type": "boolean" + }, + "idempotent": { + "type": "boolean" + } + }, + "required": [ + "forms" + ], + "additionalProperties": true + }, + "event_element": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_event" + } + }, + "uriVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "subscription": { + "$ref": "#/definitions/dataSchema" + }, + "data": { + "$ref": "#/definitions/dataSchema" + }, + "cancellation": { + "$ref": "#/definitions/dataSchema" + } + }, + "required": [ + "forms" + ], + "additionalProperties": true + }, + "link_element": { + "type": "object", + "properties": { + "href": { + "$ref": "#/definitions/anyUri" + }, + "type": { + "type": "string" + }, + "rel": { + "type": "string" + }, + "anchor": { + "$ref": "#/definitions/anyUri" + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "securityScheme": { + "oneOf": [{ + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "nosec" + ] + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "basic" + ] + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "digest" + ] + }, + "qop": { + "type": "string", + "enum": [ + "auth", + "auth-int" + ] + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "apikey" + ] + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "bearer" + ] + }, + "authorization": { + "$ref": "#/definitions/anyUri" + }, + "alg": { + "type": "string" + }, + "format": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "psk" + ] + }, + "identity": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "authorization": { + "$ref": "#/definitions/anyUri" + }, + "token": { + "$ref": "#/definitions/anyUri" + }, + "refresh": { + "$ref": "#/definitions/anyUri" + }, + "scopes": { + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "flow": { + "type": "string", + "enum": [ + "code" + ] + } + }, + "required": [ + "scheme" + ] + } + ] + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/property_element" + } + }, + "actions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/action_element" + } + }, + "events": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/event_element" + } + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "version": { + "type": "object", + "properties": { + "instance": { + "type": "string" + } + }, + "required": [ + "instance" + ] + }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/link_element" + } + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_root" + } + }, + "base": { + "$ref": "#/definitions/anyUri" + }, + "securityDefinitions": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/definitions/securityScheme" + } + }, + "support": { + "$ref": "#/definitions/anyUri" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "modified": { + "type": "string", + "format": "date-time" + }, + "security": { + "oneOf": [{ + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + }, + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "@context": { + "$ref": "#/definitions/thing-context" + } + }, + "required": [ + "title", + "security", + "securityDefinitions", + "@context" + ], + "additionalProperties": true +} \ No newline at end of file