diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00a08c48..dad8328a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: check-toml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + rev: v0.4.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/changelog.d/20240530_151558_danfuchs_notebook_refresh.md b/changelog.d/20240530_151558_danfuchs_notebook_refresh.md new file mode 100644 index 00000000..1777d493 --- /dev/null +++ b/changelog.d/20240530_151558_danfuchs_notebook_refresh.md @@ -0,0 +1,6 @@ +### New features + +- `NotebookRunner` flocks can now pick up changes to their notebooks without having to restart the whole mobu process. This refresh can happen via: + - GitHub `push` webhook post to `/mobu/github/webhook` with changes to a repo and branch that matches the flock config + - `monkeyflocker refresh ` + - `POST` to `/mobu/flocks/{flock}/refresh` diff --git a/requirements/dev.txt b/requirements/dev.txt index 5bd8412c..85c7533e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile --generate-hashes --output-file requirements/dev.txt requirements/dev.in -anyio==4.3.0 \ - --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ - --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 +anyio==4.4.0 \ + --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 \ + --hash=sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7 # via # -c requirements/main.txt # httpx @@ -131,59 +131,59 @@ click-log==0.4.0 \ --hash=sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975 \ --hash=sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756 # via scriv -coverage==7.5.1 \ - --hash=sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de \ - --hash=sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661 \ - --hash=sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26 \ - --hash=sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41 \ - --hash=sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d \ - --hash=sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981 \ - --hash=sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2 \ - --hash=sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34 \ - --hash=sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f \ - --hash=sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a \ - --hash=sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35 \ - --hash=sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223 \ - --hash=sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1 \ - --hash=sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746 \ - --hash=sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90 \ - --hash=sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c \ - --hash=sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca \ - --hash=sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8 \ - --hash=sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596 \ - --hash=sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e \ - --hash=sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd \ - --hash=sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e \ - --hash=sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3 \ - --hash=sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e \ - --hash=sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312 \ - --hash=sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7 \ - --hash=sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572 \ - --hash=sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428 \ - --hash=sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f \ - --hash=sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07 \ - --hash=sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e \ - --hash=sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4 \ - --hash=sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136 \ - --hash=sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5 \ - --hash=sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8 \ - --hash=sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d \ - --hash=sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228 \ - --hash=sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206 \ - --hash=sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa \ - --hash=sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e \ - --hash=sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be \ - --hash=sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5 \ - --hash=sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668 \ - --hash=sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601 \ - --hash=sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057 \ - --hash=sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146 \ - --hash=sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f \ - --hash=sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8 \ - --hash=sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7 \ - --hash=sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987 \ - --hash=sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19 \ - --hash=sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece +coverage==7.5.3 \ + --hash=sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523 \ + --hash=sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f \ + --hash=sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d \ + --hash=sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb \ + --hash=sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0 \ + --hash=sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c \ + --hash=sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98 \ + --hash=sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83 \ + --hash=sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8 \ + --hash=sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7 \ + --hash=sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac \ + --hash=sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84 \ + --hash=sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb \ + --hash=sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3 \ + --hash=sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884 \ + --hash=sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614 \ + --hash=sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd \ + --hash=sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807 \ + --hash=sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd \ + --hash=sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8 \ + --hash=sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc \ + --hash=sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db \ + --hash=sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0 \ + --hash=sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08 \ + --hash=sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232 \ + --hash=sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d \ + --hash=sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a \ + --hash=sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1 \ + --hash=sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286 \ + --hash=sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303 \ + --hash=sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341 \ + --hash=sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84 \ + --hash=sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45 \ + --hash=sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc \ + --hash=sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec \ + --hash=sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd \ + --hash=sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155 \ + --hash=sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52 \ + --hash=sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d \ + --hash=sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485 \ + --hash=sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31 \ + --hash=sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d \ + --hash=sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d \ + --hash=sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d \ + --hash=sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85 \ + --hash=sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce \ + --hash=sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb \ + --hash=sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974 \ + --hash=sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24 \ + --hash=sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56 \ + --hash=sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9 \ + --hash=sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35 # via # -r requirements/dev.in # pytest-cov @@ -344,9 +344,9 @@ mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 # via mypy -nodeenv==1.8.0 \ - --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \ - --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec +nodeenv==1.9.0 \ + --hash=sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1 \ + --hash=sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a # via pre-commit packaging==24.0 \ --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ @@ -442,9 +442,9 @@ pyyaml==6.0.1 \ # via # -c requirements/main.txt # pre-commit -requests==2.32.1 \ - --hash=sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5 \ - --hash=sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685 +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # -c requirements/main.txt # scriv @@ -452,33 +452,29 @@ respx==0.21.1 \ --hash=sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20 \ --hash=sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af # via -r requirements/dev.in -ruff==0.4.4 \ - --hash=sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768 \ - --hash=sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6 \ - --hash=sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6 \ - --hash=sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae \ - --hash=sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15 \ - --hash=sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab \ - --hash=sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef \ - --hash=sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95 \ - --hash=sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e \ - --hash=sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595 \ - --hash=sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36 \ - --hash=sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85 \ - --hash=sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd \ - --hash=sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891 \ - --hash=sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9 \ - --hash=sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876 \ - --hash=sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af +ruff==0.4.6 \ + --hash=sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f \ + --hash=sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50 \ + --hash=sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc \ + --hash=sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79 \ + --hash=sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a \ + --hash=sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786 \ + --hash=sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0 \ + --hash=sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259 \ + --hash=sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c \ + --hash=sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6 \ + --hash=sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062 \ + --hash=sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a \ + --hash=sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7 \ + --hash=sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618 \ + --hash=sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef \ + --hash=sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5 \ + --hash=sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f # via -r requirements/dev.in scriv==1.5.1 \ --hash=sha256:30ae9ff8d144f8e0cf394c4e1d379542f1b3823767642955b54ec40dc00b32b6 \ --hash=sha256:a3adc657733b4124fcb54527a5f3daab0d3c300de82d0fd2b9b297b243151b78 # via -r requirements/dev.in -setuptools==70.0.0 \ - --hash=sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4 \ - --hash=sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0 - # via nodeenv sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc @@ -495,13 +491,13 @@ types-pyyaml==6.0.12.20240311 \ --hash=sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342 \ --hash=sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6 # via -r requirements/dev.in -types-requests==2.32.0.20240521 \ - --hash=sha256:ab728ba43ffb073db31f21202ecb97db8753ded4a9dc49cb480d8a5350c5c421 \ - --hash=sha256:c5c4a0ae95aad51f1bf6dae9eed04a78f7f2575d4b171da37b622e08b93eb5d3 +types-requests==2.32.0.20240523 \ + --hash=sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57 \ + --hash=sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec # via -r requirements/dev.in -typing-extensions==4.11.0 \ - --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \ - --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a +typing-extensions==4.12.0 \ + --hash=sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8 \ + --hash=sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594 # via # -c requirements/main.txt # mypy diff --git a/requirements/main.in b/requirements/main.in index f0ea49de..826bd72c 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -13,6 +13,7 @@ uvicorn[standard] # Other dependencies. aiojobs click!=8.1.4,!=8.1.5 # see https://github.com/pallets/click/issues/2558 +gidgethub httpx httpx-sse jinja2 diff --git a/requirements/main.txt b/requirements/main.txt index fe4c503c..681ff408 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -8,9 +8,9 @@ annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via pydantic -anyio==4.3.0 \ - --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ - --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 +anyio==4.4.0 \ + --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 \ + --hash=sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7 # via # httpx # starlette @@ -39,9 +39,9 @@ astropy==6.1.0 \ --hash=sha256:f6bf9990282c5732c8fca053c5c54116cbf79b6a7a50549f0f1f8e156d614496 \ --hash=sha256:ff4ee9ee8163808c7fef326543f02f23c59ebf22aa6469171a141c1628739c55 # via pyvo -astropy-iers-data==0.2024.5.20.0.29.40 \ - --hash=sha256:7fff3d3404ae8560533ac0e685db7acc02c4d8984faa4ac3d607096879fba2d1 \ - --hash=sha256:8e0d91d8449f3e4c3c61354f7457629806fa2340ed144c08ab64ef424ede1d30 +astropy-iers-data==0.2024.5.27.0.30.8 \ + --hash=sha256:65cd7239571d8f73fe78fc1b21bf5b2e184ff0892abaa15364c54d016ec5e0bf \ + --hash=sha256:839f473b8ac6f395c5e060d6df95f3b5d83b271a76391d6e87b496dd73423436 # via astropy certifi==2024.2.2 \ --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ @@ -261,7 +261,9 @@ fastapi-cli==0.0.4 \ gidgethub==5.3.0 \ --hash=sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0 \ --hash=sha256:9ece7d37fbceb819b80560e7ed58f936e48a65d37ec5f56db79145156b426a25 - # via safir + # via + # -r requirements/main.in + # safir h11==0.14.0 \ --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 @@ -501,94 +503,94 @@ pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi -pydantic==2.7.1 \ - --hash=sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5 \ - --hash=sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc +pydantic==2.7.2 \ + --hash=sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7 \ + --hash=sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7 # via # -r requirements/main.in # fastapi # pydantic-settings # safir -pydantic-core==2.18.2 \ - --hash=sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b \ - --hash=sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a \ - --hash=sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90 \ - --hash=sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d \ - --hash=sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e \ - --hash=sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d \ - --hash=sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027 \ - --hash=sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804 \ - --hash=sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347 \ - --hash=sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400 \ - --hash=sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3 \ - --hash=sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399 \ - --hash=sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349 \ - --hash=sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd \ - --hash=sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c \ - --hash=sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e \ - --hash=sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413 \ - --hash=sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3 \ - --hash=sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e \ - --hash=sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3 \ - --hash=sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91 \ - --hash=sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce \ - --hash=sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c \ - --hash=sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb \ - --hash=sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664 \ - --hash=sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6 \ - --hash=sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd \ - --hash=sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3 \ - --hash=sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af \ - --hash=sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043 \ - --hash=sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350 \ - --hash=sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7 \ - --hash=sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0 \ - --hash=sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563 \ - --hash=sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761 \ - --hash=sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72 \ - --hash=sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3 \ - --hash=sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb \ - --hash=sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788 \ - --hash=sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b \ - --hash=sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c \ - --hash=sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038 \ - --hash=sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250 \ - --hash=sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec \ - --hash=sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c \ - --hash=sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74 \ - --hash=sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81 \ - --hash=sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439 \ - --hash=sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75 \ - --hash=sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0 \ - --hash=sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8 \ - --hash=sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150 \ - --hash=sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438 \ - --hash=sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae \ - --hash=sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857 \ - --hash=sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038 \ - --hash=sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374 \ - --hash=sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f \ - --hash=sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241 \ - --hash=sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592 \ - --hash=sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4 \ - --hash=sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d \ - --hash=sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b \ - --hash=sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b \ - --hash=sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182 \ - --hash=sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e \ - --hash=sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641 \ - --hash=sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70 \ - --hash=sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9 \ - --hash=sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a \ - --hash=sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543 \ - --hash=sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b \ - --hash=sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f \ - --hash=sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38 \ - --hash=sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845 \ - --hash=sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2 \ - --hash=sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0 \ - --hash=sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4 \ - --hash=sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242 +pydantic-core==2.18.3 \ + --hash=sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9 \ + --hash=sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b \ + --hash=sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426 \ + --hash=sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc \ + --hash=sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9 \ + --hash=sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc \ + --hash=sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4 \ + --hash=sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702 \ + --hash=sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6 \ + --hash=sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231 \ + --hash=sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083 \ + --hash=sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60 \ + --hash=sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7 \ + --hash=sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a \ + --hash=sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4 \ + --hash=sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6 \ + --hash=sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39 \ + --hash=sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0 \ + --hash=sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026 \ + --hash=sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c \ + --hash=sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442 \ + --hash=sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7 \ + --hash=sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044 \ + --hash=sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd \ + --hash=sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94 \ + --hash=sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361 \ + --hash=sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af \ + --hash=sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4 \ + --hash=sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022 \ + --hash=sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7 \ + --hash=sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd \ + --hash=sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8 \ + --hash=sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2 \ + --hash=sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5 \ + --hash=sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be \ + --hash=sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c \ + --hash=sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9 \ + --hash=sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417 \ + --hash=sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1 \ + --hash=sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326 \ + --hash=sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4 \ + --hash=sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb \ + --hash=sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34 \ + --hash=sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d \ + --hash=sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9 \ + --hash=sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812 \ + --hash=sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558 \ + --hash=sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779 \ + --hash=sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be \ + --hash=sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c \ + --hash=sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe \ + --hash=sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0 \ + --hash=sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c \ + --hash=sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2 \ + --hash=sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a \ + --hash=sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab \ + --hash=sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048 \ + --hash=sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3 \ + --hash=sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106 \ + --hash=sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d \ + --hash=sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb \ + --hash=sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6 \ + --hash=sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b \ + --hash=sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef \ + --hash=sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5 \ + --hash=sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb \ + --hash=sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c \ + --hash=sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06 \ + --hash=sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059 \ + --hash=sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943 \ + --hash=sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1 \ + --hash=sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2 \ + --hash=sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c \ + --hash=sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522 \ + --hash=sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f \ + --hash=sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78 \ + --hash=sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7 \ + --hash=sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a \ + --hash=sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5 # via pydantic pydantic-settings==2.2.1 \ --hash=sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed \ @@ -625,9 +627,9 @@ python-multipart==0.0.9 \ --hash=sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026 \ --hash=sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215 # via fastapi -pyvo==1.5.1 \ - --hash=sha256:0720810fe7b766ba53d10e9d9e4bb23bc967c151a6f392e22a6cbfe0d453a632 \ - --hash=sha256:53bdedd06bb37e7d9dca899fe1cc067dc423f3585c6e4799b371f04a5555720e +pyvo==1.5.2 \ + --hash=sha256:b8a24c44dace5c607b1d93afd0257d15fa109f1e865772347ea949eea01c8f71 \ + --hash=sha256:f4306d4e8f21c604dbd5df65ce101101633d62d854b0fc9c7746f342877e99f6 # via -r requirements/main.in pyyaml==6.0.1 \ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ @@ -685,9 +687,9 @@ pyyaml==6.0.1 \ # -r requirements/main.in # astropy # uvicorn -requests==2.32.1 \ - --hash=sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5 \ - --hash=sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685 +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via pyvo rich==13.7.1 \ --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ @@ -718,9 +720,9 @@ starlette==0.37.2 \ # -r requirements/main.in # fastapi # safir -structlog==24.1.0 \ - --hash=sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d \ - --hash=sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16 +structlog==24.2.0 \ + --hash=sha256:0e3fe74924a6d8857d3f612739efb94c72a7417d7c7c008d12276bca3b5bf13b \ + --hash=sha256:983bd49f70725c5e1e3867096c0c09665918936b3db27341b41d294283d7a48a # via # -r requirements/main.in # safir @@ -728,9 +730,9 @@ typer==0.12.3 \ --hash=sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914 \ --hash=sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482 # via fastapi-cli -typing-extensions==4.11.0 \ - --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \ - --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a +typing-extensions==4.12.0 \ + --hash=sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8 \ + --hash=sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594 # via # fastapi # pydantic @@ -824,9 +826,9 @@ urllib3==2.2.1 \ --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 # via requests -uvicorn==0.29.0 \ - --hash=sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de \ - --hash=sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0 +uvicorn==0.30.0 \ + --hash=sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab \ + --hash=sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37 # via # -r requirements/main.in # fastapi @@ -863,82 +865,82 @@ uvloop==0.19.0 \ --hash=sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7 \ --hash=sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256 # via uvicorn -watchfiles==0.21.0 \ - --hash=sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc \ - --hash=sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365 \ - --hash=sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0 \ - --hash=sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e \ - --hash=sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124 \ - --hash=sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c \ - --hash=sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317 \ - --hash=sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094 \ - --hash=sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7 \ - --hash=sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235 \ - --hash=sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c \ - --hash=sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c \ - --hash=sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c \ - --hash=sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235 \ - --hash=sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293 \ - --hash=sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa \ - --hash=sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef \ - --hash=sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19 \ - --hash=sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8 \ - --hash=sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d \ - --hash=sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915 \ - --hash=sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429 \ - --hash=sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097 \ - --hash=sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe \ - --hash=sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0 \ - --hash=sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d \ - --hash=sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99 \ - --hash=sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1 \ - --hash=sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a \ - --hash=sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895 \ - --hash=sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94 \ - --hash=sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562 \ - --hash=sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab \ - --hash=sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360 \ - --hash=sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1 \ - --hash=sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7 \ - --hash=sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f \ - --hash=sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03 \ - --hash=sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01 \ - --hash=sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58 \ - --hash=sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052 \ - --hash=sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e \ - --hash=sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765 \ - --hash=sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6 \ - --hash=sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137 \ - --hash=sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85 \ - --hash=sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca \ - --hash=sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f \ - --hash=sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214 \ - --hash=sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7 \ - --hash=sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7 \ - --hash=sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3 \ - --hash=sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b \ - --hash=sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7 \ - --hash=sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6 \ - --hash=sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994 \ - --hash=sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9 \ - --hash=sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec \ - --hash=sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128 \ - --hash=sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c \ - --hash=sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2 \ - --hash=sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078 \ - --hash=sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3 \ - --hash=sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e \ - --hash=sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a \ - --hash=sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6 \ - --hash=sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49 \ - --hash=sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b \ - --hash=sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28 \ - --hash=sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9 \ - --hash=sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586 \ - --hash=sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400 \ - --hash=sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165 \ - --hash=sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303 \ - --hash=sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d +watchfiles==0.22.0 \ + --hash=sha256:00095dd368f73f8f1c3a7982a9801190cc88a2f3582dd395b289294f8975172b \ + --hash=sha256:00ad0bcd399503a84cc688590cdffbe7a991691314dde5b57b3ed50a41319a31 \ + --hash=sha256:00f39592cdd124b4ec5ed0b1edfae091567c72c7da1487ae645426d1b0ffcad1 \ + --hash=sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab \ + --hash=sha256:052d668a167e9fc345c24203b104c313c86654dd6c0feb4b8a6dfc2462239249 \ + --hash=sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd \ + --hash=sha256:0b04a2cbc30e110303baa6d3ddce8ca3664bc3403be0f0ad513d1843a41c97d1 \ + --hash=sha256:0bc3b2f93a140df6806c8467c7f51ed5e55a931b031b5c2d7ff6132292e803d6 \ + --hash=sha256:0c8e0aa0e8cc2a43561e0184c0513e291ca891db13a269d8d47cb9841ced7c71 \ + --hash=sha256:103622865599f8082f03af4214eaff90e2426edff5e8522c8f9e93dc17caee13 \ + --hash=sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171 \ + --hash=sha256:1cc0cba54f47c660d9fa3218158b8963c517ed23bd9f45fe463f08262a4adae1 \ + --hash=sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1 \ + --hash=sha256:213792c2cd3150b903e6e7884d40660e0bcec4465e00563a5fc03f30ea9c166c \ + --hash=sha256:25c817ff2a86bc3de3ed2df1703e3d24ce03479b27bb4527c57e722f8554d971 \ + --hash=sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb \ + --hash=sha256:280a4afbc607cdfc9571b9904b03a478fc9f08bbeec382d648181c695648202f \ + --hash=sha256:28324d6b28bcb8d7c1041648d7b63be07a16db5510bea923fc80b91a2a6cbed6 \ + --hash=sha256:28585744c931576e535860eaf3f2c0ec7deb68e3b9c5a85ca566d69d36d8dd27 \ + --hash=sha256:28f393c1194b6eaadcdd8f941307fc9bbd7eb567995232c830f6aef38e8a6e88 \ + --hash=sha256:2abeb79209630da981f8ebca30a2c84b4c3516a214451bfc5f106723c5f45843 \ + --hash=sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a \ + --hash=sha256:2f350cbaa4bb812314af5dab0eb8d538481e2e2279472890864547f3fe2281ed \ + --hash=sha256:3218a6f908f6a276941422b035b511b6d0d8328edd89a53ae8c65be139073f84 \ + --hash=sha256:3973145235a38f73c61474d56ad6199124e7488822f3a4fc97c72009751ae3b0 \ + --hash=sha256:3a0d883351a34c01bd53cfa75cd0292e3f7e268bacf2f9e33af4ecede7e21d1d \ + --hash=sha256:425440e55cd735386ec7925f64d5dde392e69979d4c8459f6bb4e920210407f2 \ + --hash=sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797 \ + --hash=sha256:4cc382083afba7918e32d5ef12321421ef43d685b9a67cc452a6e6e18920890e \ + --hash=sha256:52fc9b0dbf54d43301a19b236b4a4614e610605f95e8c3f0f65c3a456ffd7d35 \ + --hash=sha256:55b7cc10261c2786c41d9207193a85c1db1b725cf87936df40972aab466179b6 \ + --hash=sha256:581f0a051ba7bafd03e17127735d92f4d286af941dacf94bcf823b101366249e \ + --hash=sha256:5834e1f8b71476a26df97d121c0c0ed3549d869124ed2433e02491553cb468c2 \ + --hash=sha256:5e45fb0d70dda1623a7045bd00c9e036e6f1f6a85e4ef2c8ae602b1dfadf7550 \ + --hash=sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e \ + --hash=sha256:68fe0c4d22332d7ce53ad094622b27e67440dacefbaedd29e0794d26e247280c \ + --hash=sha256:72a44e9481afc7a5ee3291b09c419abab93b7e9c306c9ef9108cb76728ca58d2 \ + --hash=sha256:7a74436c415843af2a769b36bf043b6ccbc0f8d784814ba3d42fc961cdb0a9dc \ + --hash=sha256:8597b6f9dc410bdafc8bb362dac1cbc9b4684a8310e16b1ff5eee8725d13dcd6 \ + --hash=sha256:8c39987a1397a877217be1ac0fb1d8b9f662c6077b90ff3de2c05f235e6a8f96 \ + --hash=sha256:8c3e3675e6e39dc59b8fe5c914a19d30029e36e9f99468dddffd432d8a7b1c93 \ + --hash=sha256:8dc1fc25a1dedf2dd952909c8e5cb210791e5f2d9bc5e0e8ebc28dd42fed7562 \ + --hash=sha256:8fdebb655bb1ba0122402352b0a4254812717a017d2dc49372a1d47e24073795 \ + --hash=sha256:9165bcab15f2b6d90eedc5c20a7f8a03156b3773e5fb06a790b54ccecdb73385 \ + --hash=sha256:94ebe84a035993bb7668f58a0ebf998174fb723a39e4ef9fce95baabb42b787f \ + --hash=sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848 \ + --hash=sha256:96eec15e5ea7c0b6eb5bfffe990fc7c6bd833acf7e26704eb18387fb2f5fd087 \ + --hash=sha256:97b94e14b88409c58cdf4a8eaf0e67dfd3ece7e9ce7140ea6ff48b0407a593ec \ + --hash=sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb \ + --hash=sha256:a8a31bfd98f846c3c284ba694c6365620b637debdd36e46e1859c897123aa232 \ + --hash=sha256:a927b3034d0672f62fb2ef7ea3c9fc76d063c4b15ea852d1db2dc75fe2c09696 \ + --hash=sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2 \ + --hash=sha256:aec83c3ba24c723eac14225194b862af176d52292d271c98820199110e31141e \ + --hash=sha256:b44b70850f0073b5fcc0b31ede8b4e736860d70e2dbf55701e05d3227a154a67 \ + --hash=sha256:b610fb5e27825b570554d01cec427b6620ce9bd21ff8ab775fc3a32f28bba63e \ + --hash=sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68 \ + --hash=sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb \ + --hash=sha256:bd4c06100bce70a20c4b81e599e5886cf504c9532951df65ad1133e508bf20be \ + --hash=sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71 \ + --hash=sha256:c49b76a78c156979759d759339fb62eb0549515acfe4fd18bb151cc07366629c \ + --hash=sha256:c4a65474fd2b4c63e2c18ac67a0c6c66b82f4e73e2e4d940f837ed3d2fd9d4da \ + --hash=sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39 \ + --hash=sha256:c668228833c5619f6618699a2c12be057711b0ea6396aeaece4ded94184304ea \ + --hash=sha256:c7b978c384e29d6c7372209cbf421d82286a807bbcdeb315427687f8371c340a \ + --hash=sha256:d048ad5d25b363ba1d19f92dcf29023988524bee6f9d952130b316c5802069cb \ + --hash=sha256:d3e1f3cf81f1f823e7874ae563457828e940d75573c8fbf0ee66818c8b6a9099 \ + --hash=sha256:d47e9ef1a94cc7a536039e46738e17cce058ac1593b2eccdede8bf72e45f372a \ + --hash=sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538 \ + --hash=sha256:dc1b9b56f051209be458b87edb6856a449ad3f803315d87b2da4c93b43a6fe72 \ + --hash=sha256:dc2e8fe41f3cac0660197d95216c42910c2b7e9c70d48e6d84e22f577d106fc1 \ + --hash=sha256:dc92d2d2706d2b862ce0568b24987eba51e17e14b79a1abcd2edc39e48e743c8 \ + --hash=sha256:dd64f3a4db121bc161644c9e10a9acdb836853155a108c2446db2f5ae1778c3d \ + --hash=sha256:e0f0a874231e2839abbf473256efffe577d6ee2e3bfa5b540479e892e47c172d \ + --hash=sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c \ + --hash=sha256:fe82d13461418ca5e5a808a9e40f79c1879351fcaeddbede094028e74d836e86 # via uvicorn websockets==12.0 \ --hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \ diff --git a/src/mobu/config.py b/src/mobu/config.py index 3ab3fe22..86f894a7 100644 --- a/src/mobu/config.py +++ b/src/mobu/config.py @@ -65,6 +65,17 @@ class Configuration(BaseSettings): examples=["gt-vilSCi1ifK_MyuaQgMD2dQ.d6SIJhowv5Hs3GvujOyUig"], ) + github_webhook_secret: str | None = Field( + None, + title="Github webhook secret", + description=( + "Any repo that wants mobu to automatically respawn labs when" + " notebooks change must use this secret in its webhook" + " configuration in GitHub." + ), + validation_alias="MOBU_GITHUB_WEBHOOK_SECRET", + ) + name: str = Field( "mobu", title="Name of application", diff --git a/src/mobu/constants.py b/src/mobu/constants.py index 349e6eb8..a9b96366 100644 --- a/src/mobu/constants.py +++ b/src/mobu/constants.py @@ -5,6 +5,7 @@ from datetime import timedelta __all__ = [ + "GITHUB_WEBHOOK_WAIT_SECONDS", "NOTEBOOK_REPO_URL", "NOTEBOOK_REPO_BRANCH", "TOKEN_LIFETIME", @@ -12,6 +13,9 @@ "WEBSOCKET_OPEN_TIMEOUT", ] +GITHUB_WEBHOOK_WAIT_SECONDS = 1 +"""GithHub needs some time to actually be in the state in a webhook payload.""" + NOTEBOOK_REPO_URL = "https://github.com/lsst-sqre/notebook-demo.git" """Default notebook repository for NotebookRunner.""" diff --git a/src/mobu/dependencies/context.py b/src/mobu/dependencies/context.py index e3de4768..95324242 100644 --- a/src/mobu/dependencies/context.py +++ b/src/mobu/dependencies/context.py @@ -7,11 +7,12 @@ """ from dataclasses import dataclass -from typing import Annotated +from typing import Annotated, Any from fastapi import Depends, Request from safir.dependencies.gafaelfawr import auth_logger_dependency from safir.dependencies.http_client import http_client_dependency +from safir.dependencies.logger import logger_dependency from structlog.stdlib import BoundLogger from ..factory import Factory, ProcessContext @@ -21,6 +22,7 @@ "ContextDependency", "RequestContext", "context_dependency", + "anonymous_context_dependency", ] @@ -40,6 +42,17 @@ class RequestContext: factory: Factory """Component factory.""" + def rebind_logger(self, **values: Any) -> None: + """Add the given values to the logging context. + + Parameters + ---------- + **values + Additional values that should be added to the logging context. + """ + self.logger = self.logger.bind(**values) + self.factory.set_logger(self.logger) + class ContextDependency: """Provide a per-request context as a FastAPI dependency. @@ -90,3 +103,11 @@ async def aclose(self) -> None: context_dependency = ContextDependency() """The dependency that will return the per-request context.""" + + +async def anonymous_context_dependency( + request: Request, + logger: Annotated[BoundLogger, Depends(logger_dependency)], +) -> RequestContext: + """Per-request context for non-gafaelfawr-auth'd requests.""" + return await context_dependency(request=request, logger=logger) diff --git a/src/mobu/factory.py b/src/mobu/factory.py index bc2b5bb9..0b20f89c 100644 --- a/src/mobu/factory.py +++ b/src/mobu/factory.py @@ -101,3 +101,16 @@ def create_solitary(self, solitary_config: SolitaryConfig) -> Solitary: http_client=self._context.http_client, logger=self._logger, ) + + def set_logger(self, logger: BoundLogger) -> None: + """Replace the internal logger. + + Used by the context dependency to update the logger for all + newly-created components when it's rebound with additional context. + + Parameters + ---------- + logger + New logger. + """ + self._logger = logger diff --git a/src/mobu/handlers/external.py b/src/mobu/handlers/external.py index c484c9a9..87fc5324 100644 --- a/src/mobu/handlers/external.py +++ b/src/mobu/handlers/external.py @@ -104,6 +104,20 @@ async def get_flock( return context.manager.get_flock(flock).dump() +@external_router.post( + "/flocks/{flock}/refresh", + responses={404: {"description": "Flock not found", "model": ErrorModel}}, + status_code=202, + summary="Signal a flock to refresh", +) +async def refresh_flock( + flock: str, + context: Annotated[RequestContext, Depends(context_dependency)], +) -> None: + context.logger.info("Signaling flock to refresh", flock=flock) + context.manager.refresh_flock(flock) + + @external_router.delete( "/flocks/{flock}", responses={404: {"description": "Flock not found", "model": ErrorModel}}, diff --git a/src/mobu/handlers/github.py b/src/mobu/handlers/github.py new file mode 100644 index 00000000..c21649e4 --- /dev/null +++ b/src/mobu/handlers/github.py @@ -0,0 +1,45 @@ +"""Handlers for requests from GitHub, ``/mobu/github``.""" + +import asyncio +from typing import Annotated + +from fastapi import APIRouter, Depends +from gidgethub.sansio import Event +from safir.slack.webhook import SlackRouteErrorHandler + +from ..config import config +from ..constants import GITHUB_WEBHOOK_WAIT_SECONDS +from ..dependencies.context import RequestContext, anonymous_context_dependency +from .github_webhooks import webhook_router + +github_router = APIRouter(route_class=SlackRouteErrorHandler) + + +@github_router.post( + "/webhook", + summary="GitHub webhooks", + description="This endpoint receives webhook events from GitHub.", + status_code=202, +) +async def post_github_webhook( + context: Annotated[RequestContext, Depends(anonymous_context_dependency)], +) -> None: + """Process GitHub webhook events. + + This should be exposed via a Gafaelfawr anonymous ingress. + """ + webhook_secret = config.github_webhook_secret + body = await context.request.body() + event = Event.from_http( + context.request.headers, body, secret=webhook_secret + ) + + # Bind the X-GitHub-Delivery header to the logger context; this + # identifies the webhook request in GitHub's API and UI for + # diagnostics + context.rebind_logger(github_delivery=event.delivery_id) + + context.logger.debug("Received GitHub webhook", payload=event.data) + # Give GitHub some time to reach internal consistency. + await asyncio.sleep(GITHUB_WEBHOOK_WAIT_SECONDS) + await webhook_router.dispatch(event=event, context=context) diff --git a/src/mobu/handlers/github_webhooks.py b/src/mobu/handlers/github_webhooks.py new file mode 100644 index 00000000..5e027ce8 --- /dev/null +++ b/src/mobu/handlers/github_webhooks.py @@ -0,0 +1,39 @@ +"""Github webhook handlers.""" + +from gidgethub import routing +from gidgethub.sansio import Event + +from ..dependencies.context import RequestContext + +__all__ = ["webhook_router"] + +webhook_router = routing.Router() + + +@webhook_router.register("push") +async def handle_push(event: Event, context: RequestContext) -> None: + """Handle a push event.""" + ref = event.data["ref"] + url = event.data["repository"]["clone_url"] + context.rebind_logger(ref=ref, url=url) + + prefix, branch = ref.rsplit("/", 1) + if prefix != "refs/heads": + context.logger.debug( + "github webhook ignored: ref is not a branch", + ) + return + + flocks = context.manager.list_flocks_for_repo( + repo_url=url, repo_branch=branch + ) + if not flocks: + context.logger.debug( + "github webhook ignored: no flocks match repo and branch", + ) + return + + for flock in flocks: + context.manager.refresh_flock(flock) + + context.logger.info("github webhook handled") diff --git a/src/mobu/main.py b/src/mobu/main.py index 036be927..0867f253 100644 --- a/src/mobu/main.py +++ b/src/mobu/main.py @@ -25,6 +25,7 @@ from .config import config from .dependencies.context import context_dependency from .handlers.external import external_router +from .handlers.github import github_router from .handlers.internal import internal_router from .status import post_status @@ -69,6 +70,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # Attach the routers. app.include_router(internal_router) app.include_router(external_router, prefix=config.path_prefix) +app.include_router(github_router, prefix=f"{config.path_prefix}/github") # Add middleware. app.add_middleware(XForwardedMiddleware) diff --git a/src/mobu/models/business/base.py b/src/mobu/models/business/base.py index 0f8c9744..dec8df63 100644 --- a/src/mobu/models/business/base.py +++ b/src/mobu/models/business/base.py @@ -79,6 +79,10 @@ class BusinessData(BaseModel): success_count: int = Field(..., title="Number of successes", examples=[25]) + refreshing: bool = Field( + ..., title="If the business is currently in the process of refreshing" + ) + timings: list[StopwatchData] = Field(..., title="Timings of events") model_config = ConfigDict(extra="forbid") diff --git a/src/mobu/services/business/base.py b/src/mobu/services/business/base.py index 03859ea8..f233f4d4 100644 --- a/src/mobu/services/business/base.py +++ b/src/mobu/services/business/base.py @@ -97,6 +97,7 @@ def __init__( self.timings = Timings() self.control: Queue[BusinessCommand] = Queue() self.stopping = False + self.refreshing = False # Methods that should be overridden by child classes if needed. @@ -204,6 +205,9 @@ async def stop(self) -> None: await self.control.join() self.logger.info("Stopped") + def signal_refresh(self) -> None: + self.refreshing = True + # Utility functions that can be used by child classes. async def pause(self, interval: timedelta) -> bool: @@ -299,6 +303,7 @@ def dump(self) -> BusinessData: name=type(self).__name__, failure_count=self.failure_count, success_count=self.success_count, + refreshing=self.refreshing, timings=self.timings.dump(), ) diff --git a/src/mobu/services/business/notebookrunner.py b/src/mobu/services/business/notebookrunner.py index 3e7cb540..cfb24287 100644 --- a/src/mobu/services/business/notebookrunner.py +++ b/src/mobu/services/business/notebookrunner.py @@ -70,21 +70,34 @@ def annotations(self, cell_id: str | None = None) -> dict[str, str]: return result async def startup(self) -> None: + await self.initialize() + await super().startup() + + async def cleanup(self) -> None: + shutil.rmtree(str(self._repo_dir)) + self._repo_dir = None + + async def initialize(self) -> None: if self._repo_dir is None: self._repo_dir = Path(TemporaryDirectory().name) await self.clone_repo() + self._exclude_paths = { (self._repo_dir / path) for path in self.options.exclude_dirs } self._notebook_paths = self.find_notebooks() self.logger.info("Repository cloned and ready") - await super().startup() async def shutdown(self) -> None: - shutil.rmtree(str(self._repo_dir)) - self._repo_dir = None + await self.cleanup() await super().shutdown() + async def refresh(self) -> None: + self.logger.info("Recloning notebooks and forcing new execution") + await self.cleanup() + await self.initialize() + self.refreshing = False + async def clone_repo(self) -> None: url = self.options.repo_url branch = self.options.repo_branch @@ -151,6 +164,10 @@ async def open_session( async def execute_code(self, session: JupyterLabSession) -> None: for count in range(self.options.max_executions): + if self.refreshing: + await self.refresh() + return + self._notebook = self.next_notebook() iteration = f"{count + 1}/{self.options.max_executions}" diff --git a/src/mobu/services/flock.py b/src/mobu/services/flock.py index 379c74a9..9b905e98 100644 --- a/src/mobu/services/flock.py +++ b/src/mobu/services/flock.py @@ -12,6 +12,10 @@ from structlog.stdlib import BoundLogger from ..exceptions import MonkeyNotFoundError +from ..models.business.notebookrunner import ( + NotebookRunnerConfig, + NotebookRunnerOptions, +) from ..models.flock import FlockConfig, FlockData, FlockSummary from ..models.user import AuthenticatedUser, User, UserSpec from ..storage.gafaelfawr import GafaelfawrStorage @@ -130,6 +134,25 @@ async def stop(self) -> None: awaits = [m.stop() for m in self._monkeys.values()] await asyncio.gather(*awaits) + def signal_refresh(self) -> None: + """Signal all the monkeys to refresh their busniess.""" + self._logger.info("Signaling monkeys to refresh") + for monkey in self._monkeys.values(): + monkey.signal_refresh() + + def uses_repo(self, repo_url: str, repo_branch: str) -> bool: + match self._config: + case FlockConfig( + business=NotebookRunnerConfig( + options=NotebookRunnerOptions( + repo_url=url, + repo_branch=branch, + ) + ) + ) if (url, branch) == (repo_url, repo_branch): + return True + return False + def _create_monkey(self, user: AuthenticatedUser) -> Monkey: """Create a monkey that will run as a given user.""" return Monkey( diff --git a/src/mobu/services/manager.py b/src/mobu/services/manager.py index 25ce5d94..08bd04a3 100644 --- a/src/mobu/services/manager.py +++ b/src/mobu/services/manager.py @@ -118,6 +118,15 @@ def get_flock(self, name: str) -> Flock: raise FlockNotFoundError(name) return flock + def list_flocks_for_repo( + self, repo_url: str, repo_branch: str + ) -> list[str]: + return [ + name + for name, flock in self._flocks.items() + if flock.uses_repo(repo_url=repo_url, repo_branch=repo_branch) + ] + def list_flocks(self) -> list[str]: """List all flocks. @@ -156,3 +165,21 @@ async def stop_flock(self, name: str) -> None: raise FlockNotFoundError(name) del self._flocks[name] await flock.stop() + + def refresh_flock(self, name: str) -> None: + """Tell a flock to refresh. + + Parameters + ---------- + name + Name of flock to refresh. + + Raises + ------ + FlockNotFoundError + Raised if no flock was found with that name. + """ + flock = self._flocks.get(name) + if flock is None: + raise FlockNotFoundError(name) + flock.signal_refresh() diff --git a/src/mobu/services/monkey.py b/src/mobu/services/monkey.py index a06b573c..07101a2a 100644 --- a/src/mobu/services/monkey.py +++ b/src/mobu/services/monkey.py @@ -240,6 +240,10 @@ async def stop(self) -> None: self._job = None self._state = MonkeyState.FINISHED + def signal_refresh(self) -> None: + """Tell the business to refresh.""" + self.business.signal_refresh() + def dump(self) -> MonkeyData: """Return information about a running monkey.""" return MonkeyData( diff --git a/src/monkeyflocker/cli.py b/src/monkeyflocker/cli.py index 35e8d3ad..96198b46 100644 --- a/src/monkeyflocker/cli.py +++ b/src/monkeyflocker/cli.py @@ -127,3 +127,37 @@ async def stop( if output: await client.report(name, output) await client.stop(name) + + +@main.command() +@click.option( + "-e", + "--base-url", + envvar="MONKEYFLOCKER_BASE_URL", + default="http://localhost:8000", + help="URL of RSP instance to dispatch mobu workers on", +) +@click.option( + "-k", + "--token", + required=True, + envvar="MONKEYFLOCKER_TOKEN", + help="Token to use to drive mobu", +) +@click.option( + "-o", + "--output", + type=click.Path(path_type=Path), + envvar="MONKEYFLOCKER_OUTPUT", + help="Directory in which to store output", +) +@click.argument("name") +@run_with_asyncio +async def refresh( + base_url: str, token: str, output: Path | None, name: str +) -> None: + """Signal a flock to refresh.""" + async with MonkeyflockerClient(base_url, token) as client: + if output: + await client.report(name, output) + await client.refresh(name) diff --git a/src/monkeyflocker/client.py b/src/monkeyflocker/client.py index 6f7d00b2..5af68cd0 100644 --- a/src/monkeyflocker/client.py +++ b/src/monkeyflocker/client.py @@ -96,6 +96,14 @@ async def stop(self, name: str) -> None: r = await self._client.delete(url) r.raise_for_status() + async def refresh(self, name: str) -> None: + """Restart a flock of monkeys.""" + if not self._client: + raise RuntimeError("Must be used as a context manager") + url = urljoin(self._base_url, f"/mobu/flocks/{name}/refresh") + r = await self._client.post(url) + r.raise_for_status() + def _initialize_logging(self) -> BoundLogger: """Set up the monkeyflocker logger.""" formatter = logging.Formatter( diff --git a/tests/autostart_test.py b/tests/autostart_test.py index 542227b1..8e99f9ef 100644 --- a/tests/autostart_test.py +++ b/tests/autostart_test.py @@ -68,6 +68,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: "business": { "failure_count": 0, "name": "EmptyLoop", + "refreshing": False, "success_count": ANY, "timings": ANY, }, @@ -141,6 +142,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: ), }, "name": "NubladoPythonLoop", + "refreshing": False, "success_count": ANY, "timings": ANY, }, @@ -164,6 +166,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: ), }, "name": "NubladoPythonLoop", + "refreshing": False, "success_count": ANY, "timings": ANY, }, diff --git a/tests/business/gitlfs_test.py b/tests/business/gitlfs_test.py index 5a13558e..9a1c4552 100644 --- a/tests/business/gitlfs_test.py +++ b/tests/business/gitlfs_test.py @@ -31,6 +31,7 @@ async def test_run( "business": { "failure_count": 0, "name": "GitLFSBusiness", + "refreshing": False, "success_count": 1, "timings": ANY, }, @@ -70,6 +71,7 @@ async def test_fail(client: AsyncClient, respx_mock: respx.Router) -> None: "business": { "failure_count": 1, "name": "GitLFSBusiness", + "refreshing": False, "success_count": 0, "timings": ANY, }, diff --git a/tests/business/notebookrunner_test.py b/tests/business/notebookrunner_test.py index 959bd007..61c4ba28 100644 --- a/tests/business/notebookrunner_test.py +++ b/tests/business/notebookrunner_test.py @@ -15,7 +15,8 @@ from mobu.storage.git import Git from ..support.gafaelfawr import mock_gafaelfawr -from ..support.util import wait_for_business +from ..support.jupyter import MockJupyter +from ..support.util import wait_for_business, wait_for_log_message async def setup_git_repo(repo_path: Path) -> None: @@ -80,6 +81,7 @@ async def test_run( "failure_count": 0, "name": "NotebookRunner", "notebook": "test-notebook.ipynb", + "refreshing": False, "success_count": 1, "timings": ANY, }, @@ -161,6 +163,7 @@ async def test_run_recursive( "failure_count": 0, "name": "NotebookRunner", "notebook": ANY, + "refreshing": False, "success_count": 1, "timings": ANY, }, @@ -205,6 +208,87 @@ async def test_run_recursive( assert "Done with this cycle of notebooks" in r.text +@pytest.mark.asyncio +async def test_refresh( + client: AsyncClient, + jupyter: MockJupyter, + respx_mock: respx.Router, + tmp_path: Path, +) -> None: + mock_gafaelfawr(respx_mock) + cwd = Path.cwd() + + # Set up a notebook repository. + source_path = Path(__file__).parent.parent / "notebooks" + repo_path = tmp_path / "notebooks" + + shutil.copytree(str(source_path), str(repo_path)) + + # Set up git repo + await setup_git_repo(repo_path) + + # Start a monkey. We have to do this in a try/finally block since the + # runner will change working directories, which because working + # directories are process-global may mess up future tests. + try: + r = await client.put( + "/mobu/flocks", + json={ + "name": "test", + "count": 1, + "user_spec": {"username_prefix": "testuser"}, + "scopes": ["exec:notebook"], + "business": { + "type": "NotebookRunner", + "options": { + "spawn_settle_time": 0, + "execution_idle_time": 1, + "idle_time": 1, + "max_executions": 1000, + "repo_url": str(repo_path), + "repo_branch": "main", + "working_directory": str(repo_path), + }, + }, + }, + ) + assert r.status_code == 201 + + # We should see a message from the notebook execution in the logs. + assert await wait_for_log_message( + client, "testuser1", msg="This is a test" + ) + + # Change the notebook and git commit it + notebook = repo_path / "test-notebook.ipynb" + contents = notebook.read_text() + new_contents = contents.replace("This is a test", "This is a NEW test") + notebook.write_text(new_contents) + + git = Git(repo=repo_path) + await git.add(str(notebook)) + await git.commit("-m", "Updating notebook") + + jupyter.expected_session_name = "test-notebook.ipynb" + jupyter.expected_session_type = "notebook" + + # Refresh the notebook + r = await client.post("/mobu/flocks/test/refresh") + assert r.status_code == 202 + + # The refresh should have forced a new execution + assert await wait_for_log_message( + client, "testuser1", msg="Deleting lab" + ) + + # We should see a message from the updated notebook. + assert await wait_for_log_message( + client, "testuser1", msg="This is a NEW test" + ) + finally: + os.chdir(cwd) + + @pytest.mark.asyncio async def test_exclude_dirs( client: AsyncClient, respx_mock: respx.Router, tmp_path: Path @@ -261,6 +345,7 @@ async def test_exclude_dirs( "failure_count": 0, "name": "NotebookRunner", "notebook": ANY, + "refreshing": False, "success_count": 1, "timings": ANY, }, @@ -359,6 +444,7 @@ async def test_alert( }, "name": "NotebookRunner", "notebook": "exception.ipynb", + "refreshing": False, "running_code": bad_code, "success_count": 0, "timings": ANY, diff --git a/tests/business/nubladopythonloop_test.py b/tests/business/nubladopythonloop_test.py index 1e3020e5..d87c2e95 100644 --- a/tests/business/nubladopythonloop_test.py +++ b/tests/business/nubladopythonloop_test.py @@ -47,6 +47,7 @@ async def test_run( "business": { "failure_count": 0, "name": "NubladoPythonLoop", + "refreshing": False, "success_count": 1, "timings": ANY, }, diff --git a/tests/business/tapqueryrunner_test.py b/tests/business/tapqueryrunner_test.py index ed14da42..c5facc4e 100644 --- a/tests/business/tapqueryrunner_test.py +++ b/tests/business/tapqueryrunner_test.py @@ -44,6 +44,7 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: "business": { "failure_count": 0, "name": "TAPQueryRunner", + "refreshing": False, "success_count": 1, "timings": ANY, }, diff --git a/tests/business/tapquerysetrunner_test.py b/tests/business/tapquerysetrunner_test.py index 3629ab0e..d6de9da1 100644 --- a/tests/business/tapquerysetrunner_test.py +++ b/tests/business/tapquerysetrunner_test.py @@ -48,6 +48,7 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: "business": { "failure_count": 0, "name": "TAPQuerySetRunner", + "refreshing": False, "success_count": 1, "timings": ANY, }, diff --git a/tests/conftest.py b/tests/conftest.py index a377664d..ce996119 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ from collections.abc import AsyncIterator, Iterator from contextlib import asynccontextmanager -from unittest.mock import patch +from unittest.mock import DEFAULT, patch import pytest import pytest_asyncio @@ -19,7 +19,7 @@ from mobu.config import config from mobu.services.business.gitlfs import GitLFSBusiness -from .support.constants import TEST_BASE_URL +from .support.constants import TEST_BASE_URL, TEST_GITHUB_WEBHOOK_SECRET from .support.gafaelfawr import make_gafaelfawr_token from .support.gitlfs import ( no_git_lfs_data, @@ -47,6 +47,7 @@ def _configure() -> Iterator[None]: """ config.environment_url = HttpUrl("https://test.example.com") config.gafaelfawr_token = make_gafaelfawr_token() + config.github_webhook_secret = TEST_GITHUB_WEBHOOK_SECRET yield config.environment_url = None config.gafaelfawr_token = None @@ -83,6 +84,18 @@ async def client(app: FastAPI) -> AsyncIterator[AsyncClient]: yield client +@pytest_asyncio.fixture +async def anon_client(app: FastAPI) -> AsyncIterator[AsyncClient]: + """Return an anonymous ``httpx.AsyncClient`` configured to talk to the test + app. + """ + async with AsyncClient( + transport=ASGITransport(app=app), # type: ignore[arg-type] + base_url=TEST_BASE_URL, + ) as client: + yield client + + @pytest.fixture def jupyter(respx_mock: respx.Router) -> Iterator[MockJupyter]: """Mock out JupyterHub and Jupyter labs.""" @@ -132,3 +145,12 @@ def gitlfs_mock() -> Iterator[None]: autospec=True, ): yield None + + +@pytest.fixture +def no_monkey_business() -> Iterator[None]: + """Prevent any monkeys from actually doing any business.""" + with patch.multiple( + "mobu.services.flock.Monkey", start=DEFAULT, stop=DEFAULT + ): + yield None diff --git a/tests/data/github_webhook.tmpl.json b/tests/data/github_webhook.tmpl.json new file mode 100644 index 00000000..8978e531 --- /dev/null +++ b/tests/data/github_webhook.tmpl.json @@ -0,0 +1,214 @@ +{ + "ref": "$ref", + "before": "42ec8ee4ffcb3dff4241d59a80aa875784d441aa", + "after": "7917992fb012d1858bfc344034679b2aa5d81868", + "repository": { + "id": 804427678, + "node_id": "R_kgDOL_KXng", + "name": "$repo", + "full_name": "$org/$repo", + "private": false, + "owner": { + "name": "someone", + "email": "someone@somewhere.org", + "login": "some-login", + "id": 10158560, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjEwMTU4NTYw", + "avatar_url": "https://avatars.githubusercontent.com/u/10158560?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/$org", + "html_url": "https://github.com/$org", + "followers_url": "https://api.github.com/users/$org/followers", + "following_url": "https://api.github.com/users/$org/following{/other_user}", + "gists_url": "https://api.github.com/users/$org/gists{/gist_id}", + "starred_url": "https://api.github.com/users/$org/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/$org/subscriptions", + "organizations_url": "https://api.github.com/users/$org/orgs", + "repos_url": "https://api.github.com/users/$org/repos", + "events_url": "https://api.github.com/users/$org/events{/privacy}", + "received_events_url": "https://api.github.com/users/$org/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/$org/$repo", + "description": "Stuff with which to test mobu", + "fork": false, + "url": "https://github.com/$org/$repo", + "forks_url": "https://api.github.com/repos/$org/$repo/forks", + "keys_url": "https://api.github.com/repos/$org/$repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/$org/$repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/$org/$repo/teams", + "hooks_url": "https://api.github.com/repos/$org/$repo/hooks", + "issue_events_url": "https://api.github.com/repos/$org/$repo/issues/events{/number}", + "assignees_url": "https://api.github.com/repos/$org/$repo/assignees{/user}", + "events_url": "https://api.github.com/repos/$org/$repo/events", + "branches_url": "https://api.github.com/repos/$org/$repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/$org/$repo/tags", + "blobs_url": "https://api.github.com/repos/$org/$repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/$org/$repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/$org/$repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/$org/$repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/$org/$repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/$org/$repo/languages", + "stargazers_url": "https://api.github.com/repos/$org/$repo/stargazers", + "contributors_url": "https://api.github.com/repos/$org/$repo/contributors", + "subscribers_url": "https://api.github.com/repos/$org/$repo/subscribers", + "subscription_url": "https://api.github.com/repos/$org/$repo/subscription", + "commits_url": "https://api.github.com/repos/$org/$repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/$org/$repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/$org/$repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/$org/$repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/$org/$repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/$org/$repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/$org/$repo/merges", + "archive_url": "https://api.github.com/repos/$org/$repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/$org/$repo/downloads", + "issues_url": "https://api.github.com/repos/$org/$repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/$org/$repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/$org/$repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/$org/$repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/$org/$repo/labels{/name}", + "releases_url": "https://api.github.com/repos/$org/$repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/$org/$repo/deployments", + "created_at": 1716390526, + "updated_at": "2024-05-28T21:14:02Z", + "pushed_at": 1716949144, + "git_url": "git://github.com/$org/$repo.git", + "ssh_url": "git@github.com:$org/$repo.git", + "clone_url": "https://github.com/$org/$repo.git", + "svn_url": "https://github.com/$org/$repo", + "homepage": null, + "size": 13, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Jupyter Notebook", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + + ], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main", + "stargazers": 0, + "master_branch": "main", + "organization": "$org", + "custom_properties": { + + } + }, + "pusher": { + "name": "some-pusher", + "email": "330402+some-pusher@users.noreply.github.com" + }, + "organization": { + "login": "$org", + "id": 10158560, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjEwMTU4NTYw", + "url": "https://api.github.com/orgs/$org", + "repos_url": "https://api.github.com/orgs/$org/repos", + "events_url": "https://api.github.com/orgs/$org/events", + "hooks_url": "https://api.github.com/orgs/$org/hooks", + "issues_url": "https://api.github.com/orgs/$org/issues", + "members_url": "https://api.github.com/orgs/$org/members{/member}", + "public_members_url": "https://api.github.com/orgs/$org/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/10158560?v=4", + "description": "Rubin Observatory Science Quality and Reliability Engineering team" + }, + "sender": { + "login": "some-sender", + "id": 330402, + "node_id": "MDQ6VXNlcjMzMDQwMg==", + "avatar_url": "https://avatars.githubusercontent.com/u/330402?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/some-sender", + "html_url": "https://github.com/some-sender", + "followers_url": "https://api.github.com/users/some-sender/followers", + "following_url": "https://api.github.com/users/some-sender/following{/other_user}", + "gists_url": "https://api.github.com/users/some-sender/gists{/gist_id}", + "starred_url": "https://api.github.com/users/some-sender/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/some-sender/subscriptions", + "organizations_url": "https://api.github.com/users/some-sender/orgs", + "repos_url": "https://api.github.com/users/some-sender/repos", + "events_url": "https://api.github.com/users/some-sender/events{/privacy}", + "received_events_url": "https://api.github.com/users/some-sender/received_events", + "type": "User", + "site_admin": false + }, + "created": false, + "deleted": false, + "forced": false, + "base_ref": null, + "compare": "https://github.com/$org/$repo/compare/42ec8ee4ffcb...7917992fb012", + "commits": [ + { + "id": "7917992fb012d1858bfc344034679b2aa5d81868", + "tree_id": "de0a1710ea36ef179af29acb9bda3f6edb31f56c", + "distinct": true, + "message": "refresh", + "timestamp": "2024-05-28T21:19:03-05:00", + "url": "https://github.com/$org/$repo/commit/7917992fb012d1858bfc344034679b2aa5d81868", + "author": { + "name": "Some Body", + "email": "somebody@somewhere.org", + "username": "some-sender" + }, + "committer": { + "name": "Some Body", + "email": "somebody@somewhere.org", + "username": "some-sender" + }, + "added": [ + + ], + "removed": [ + + ], + "modified": [ + "simple_notebook.ipynb" + ] + } + ], + "head_commit": { + "id": "7917992fb012d1858bfc344034679b2aa5d81868", + "tree_id": "de0a1710ea36ef179af29acb9bda3f6edb31f56c", + "distinct": true, + "message": "refresh", + "timestamp": "2024-05-28T21:19:03-05:00", + "url": "https://github.com/$org/$repo/commit/7917992fb012d1858bfc344034679b2aa5d81868", + "author": { + "name": "Some Body", + "email": "somebody@somewhere.org", + "username": "some-sender" + }, + "committer": { + "name": "Some Body", + "email": "somebody@somewhere.org", + "username": "some-sender" + }, + "added": [ + + ], + "removed": [ + + ], + "modified": [ + "simple_notebook.ipynb" + ] + } +} diff --git a/tests/handlers/flock_test.py b/tests/handlers/flock_test.py index 0037c5f3..04014549 100644 --- a/tests/handlers/flock_test.py +++ b/tests/handlers/flock_test.py @@ -22,7 +22,7 @@ async def test_empty(client: AsyncClient) -> None: @pytest.mark.asyncio -async def test_start_stop( +async def test_start_stop_refresh( client: AsyncClient, respx_mock: respx.Router ) -> None: mock_gafaelfawr(respx_mock) @@ -51,6 +51,7 @@ async def test_start_stop( "business": { "failure_count": 0, "name": "EmptyLoop", + "refreshing": False, "success_count": ANY, "timings": ANY, }, @@ -75,6 +76,11 @@ async def test_start_stop( assert r.status_code == 200 assert r.json() == expected + r = await client.post("/mobu/flocks/test/refresh") + assert r.status_code == 202 + # That should've updated the refreshing status + expected["monkeys"][0]["business"]["refreshing"] = True + r = await client.get("/mobu/flocks/test/monkeys") assert r.status_code == 200 assert r.json() == ["testuser1"] @@ -108,6 +114,8 @@ async def test_start_stop( r = await client.get("/mobu/flocks/other") assert r.status_code == 404 + r = await client.post("/mobu/flocks/other/refresh") + assert r.status_code == 404 r = await client.delete("/mobu/flocks/other") assert r.status_code == 404 r = await client.get("/mobu/flocks/other/monkeys") @@ -161,6 +169,7 @@ async def test_user_list( "business": { "failure_count": 0, "name": "EmptyLoop", + "refreshing": False, "success_count": ANY, "timings": ANY, }, @@ -178,6 +187,7 @@ async def test_user_list( "business": { "failure_count": 0, "name": "EmptyLoop", + "refreshing": False, "success_count": ANY, "timings": ANY, }, diff --git a/tests/handlers/github_test.py b/tests/handlers/github_test.py new file mode 100644 index 00000000..e5d24273 --- /dev/null +++ b/tests/handlers/github_test.py @@ -0,0 +1,147 @@ +"""Test the github webhook handler.""" + +import hashlib +import hmac +from dataclasses import dataclass +from pathlib import Path +from string import Template + +import pytest +import respx +from httpx import AsyncClient + +from ..support.constants import TEST_GITHUB_WEBHOOK_SECRET +from ..support.gafaelfawr import mock_gafaelfawr + + +@dataclass(frozen=True) +class GithubRequest: + payload: str + headers: dict[str, str] + + +def webhook_request(org: str, repo: str, ref: str) -> GithubRequest: + """Build a Github webhook request and headers with the right hash.""" + data_path = Path(__file__).parent.parent / "data" + template = (data_path / "github_webhook.tmpl.json").read_text() + payload = Template(template).substitute(org=org, repo=repo, ref=ref) + + # https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries#python-example + hash_object = hmac.new( + TEST_GITHUB_WEBHOOK_SECRET.encode(), + msg=payload.encode(), + digestmod=hashlib.sha256, + ) + sig = "sha256=" + hash_object.hexdigest() + + headers = { + "Accept": "*/*", + "Content-Type": "application/json", + "User-Agent": "GitHub-Hookshot/c9d6c0a", + "X-GitHub-Delivery": "d2d3c948-1d61-11ef-848a-c578f23615c9", + "X-GitHub-Event": "push", + "X-GitHub-Hook-ID": "479971864", + "X-GitHub-Hook-Installation-Target-ID": "804427678", + "X-GitHub-Hook-Installation-Target-Type": "repository", + "X-Hub-Signature-256": sig, + } + + return GithubRequest(payload=payload, headers=headers) + + +@pytest.mark.asyncio +async def test_handle_webhook( + no_monkey_business: None, + client: AsyncClient, + anon_client: AsyncClient, + respx_mock: respx.Router, +) -> None: + configs = [ + { + "name": "test-notebook", + "count": 1, + "user_spec": {"username_prefix": "testuser-notebook"}, + "scopes": ["exec:notebook"], + "business": { + "type": "NotebookRunner", + "options": { + "repo_url": "https://github.com/lsst-sqre/some-repo.git", + "repo_branch": "main", + }, + }, + }, + { + "name": "test-notebook-branch", + "count": 1, + "user_spec": {"username_prefix": "testuser-notebook-branch"}, + "scopes": ["exec:notebook"], + "business": { + "type": "NotebookRunner", + "options": { + "repo_url": "https://github.com/lsst-sqre/some-repo.git", + "repo_branch": "some-branch", + }, + }, + }, + { + "name": "test-other-notebook", + "count": 1, + "user_spec": {"username_prefix": "testuser-other-notebook"}, + "scopes": ["exec:notebook"], + "business": { + "type": "NotebookRunner", + "options": { + "repo_url": "https://github.com/lsst-sqre/some-other-repo.git", + "repo_branch": "main", + }, + }, + }, + { + "name": "test-non-notebook", + "count": 1, + "user_spec": {"username_prefix": "testuser-non-notebook"}, + "scopes": ["exec:notebook"], + "business": {"type": "EmptyLoop"}, + }, + ] + + mock_gafaelfawr(respx_mock) + + # Start the flocks + for config in configs: + r = await client.put("/mobu/flocks", json=config) + assert r.status_code == 201 + + # Post a webhook event like GitHub would + request = webhook_request( + org="lsst-sqre", + repo="some-repo", + ref="refs/heads/main", + ) + await anon_client.post( + "/mobu/github/webhook", + headers=request.headers, + content=request.payload, + ) + + # Only the business for the correct branch and repo should be refreshing + r = await client.get( + "/mobu/flocks/test-notebook/monkeys/testuser-notebook1" + ) + assert r.json()["business"]["refreshing"] is True + + # The other businesses should not be refreshing + r = await client.get( + "/mobu/flocks/test-notebook-branch/monkeys/testuser-notebook-branch1" + ) + assert r.json()["business"]["refreshing"] is False + + r = await client.get( + "/mobu/flocks/test-other-notebook/monkeys/testuser-other-notebook1" + ) + assert r.json()["business"]["refreshing"] is False + + r = await client.get( + "/mobu/flocks/test-non-notebook/monkeys/testuser-non-notebook1" + ) + assert r.json()["business"]["refreshing"] is False diff --git a/tests/monkeyflocker_test.py b/tests/monkeyflocker_test.py index cc243f8e..b17deb68 100644 --- a/tests/monkeyflocker_test.py +++ b/tests/monkeyflocker_test.py @@ -48,7 +48,7 @@ def monkeyflocker_app(tmp_path: Path) -> Iterator[UvicornProcess]: uvicorn.process.terminate() -def test_start_report_stop( +def test_start_report_refresh_stop( tmp_path: Path, monkeyflocker_app: UvicornProcess ) -> None: runner = CliRunner() @@ -87,6 +87,7 @@ def test_start_report_stop( "business": { "failure_count": 0, "name": "EmptyLoop", + "refreshing": ANY, "success_count": ANY, "timings": ANY, }, @@ -125,6 +126,23 @@ def test_start_report_stop( assert "Idling..." in log shutil.rmtree(str(output_path)) + + result = runner.invoke( + main, + [ + "refresh", + "-e", + monkeyflocker_app.url, + "-o", + str(output_path), + "-k", + config.gafaelfawr_token, + "basic", + ], + catch_exceptions=False, + ) + assert result.exit_code == 0 + result = runner.invoke( main, [ diff --git a/tests/support/constants.py b/tests/support/constants.py index 69694516..47881223 100644 --- a/tests/support/constants.py +++ b/tests/support/constants.py @@ -2,3 +2,6 @@ TEST_BASE_URL = "https://example.com" """Base URL used for the test `httpx.AsyncClient`.""" + +TEST_GITHUB_WEBHOOK_SECRET = "some-webhook-secret" +"""Webhook secret used for hashing test github webhook payloads.""" diff --git a/tests/support/jupyter.py b/tests/support/jupyter.py index be762d5e..6e6c30a5 100644 --- a/tests/support/jupyter.py +++ b/tests/support/jupyter.py @@ -91,6 +91,8 @@ def __init__(self) -> None: self.spawn_timeout = False self.redirect_loop = False self.lab_form: dict[str, dict[str, str]] = {} + self.expected_session_name = "(no notebook)" + self.expected_session_type = "console" self._delete_at: dict[str, datetime | None] = {} self._fail: dict[str, dict[JupyterAction, bool]] = {} self._hub_xsrf = os.urandom(8).hex() @@ -278,8 +280,8 @@ def create_session(self, request: Request) -> Response: assert state == JupyterState.LAB_RUNNING body = json.loads(request.content.decode()) assert body["kernel"]["name"] == "LSST" - assert body["name"] == "(no notebook)" - assert body["type"] == "console" + assert body["name"] == self.expected_session_name + assert body["type"] == self.expected_session_type session = JupyterLabSession( session_id=uuid4().hex, kernel_id=uuid4().hex ) diff --git a/tests/support/util.py b/tests/support/util.py index 313ba2d8..4c4f0729 100644 --- a/tests/support/util.py +++ b/tests/support/util.py @@ -7,7 +7,10 @@ from httpx import AsyncClient -__all__ = ["wait_for_business"] +__all__ = [ + "wait_for_business", + "wait_for_log_message", +] async def wait_for_business( @@ -26,6 +29,19 @@ async def wait_for_business( return data +async def wait_for_log_message( + client: AsyncClient, username: str, *, flock: str = "test", msg: str +) -> bool: + """Wait until some text appears in a user's log.""" + for _ in range(1, 10): + await asyncio.sleep(0.5) + r = await client.get(f"/mobu/flocks/{flock}/monkeys/{username}/log") + assert r.status_code == 200 + if msg in r.text: + return True + return False + + async def wait_for_flock_start(client: AsyncClient, flock: str) -> None: """Wait for all the monkeys in a flock to have started.""" for _ in range(1, 10):