diff --git a/changelog.d/20231214_165219_rra_DM_42182.md b/changelog.d/20231214_165219_rra_DM_42182.md new file mode 100644 index 00000000..19908995 --- /dev/null +++ b/changelog.d/20231214_165219_rra_DM_42182.md @@ -0,0 +1,3 @@ +### Backwards-incompatible changes + +- All environment variables used to configure mobu now start with `MOBU_`, and several have changed their names. The new settings are `MOBU_ALERT_HOOK`, `MOBU_AUTOSTART_PATH`, `MOBU_ENVIRONMENT_URL`, `MOBU_GAFAELFAWR_TOKEN`, `MOBU_NAME`, `MOBU_PATH_PREFIX`, `MOBU_LOGGING_PROFILE`, and `MOBU_LOG_LEVEL`. diff --git a/requirements/main.in b/requirements/main.in index a86da356..4897762f 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -17,10 +17,11 @@ click!=8.1.4,!=8.1.5 # see https://github.com/pallets/click/issues/2558 httpx httpx-sse jinja2 -pydantic<2 +pydantic>2 +pydantic-settings pyvo pyyaml -safir>=3.8.0 +safir>=5.0.0 shortuuid structlog websockets diff --git a/requirements/main.txt b/requirements/main.txt index 0fa0cdf5..240e9984 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -8,6 +8,10 @@ aiojobs==1.2.1 \ --hash=sha256:59d6e7ad7829e9d0f73bfceeae28153b541be6b0959a08cc5ceb222717c888ff \ --hash=sha256:88efd8e56295eff69efcc44488e41c38159bcbda2ac7c6884e4140674dace54f # via -r requirements/main.in +annotated-types==0.6.0 \ + --hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \ + --hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d + # via pydantic anyio==3.7.1 \ --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \ --hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5 @@ -435,47 +439,125 @@ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pydantic==1.10.13 \ - --hash=sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548 \ - --hash=sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80 \ - --hash=sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340 \ - --hash=sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01 \ - --hash=sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132 \ - --hash=sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599 \ - --hash=sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1 \ - --hash=sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8 \ - --hash=sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe \ - --hash=sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0 \ - --hash=sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17 \ - --hash=sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953 \ - --hash=sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f \ - --hash=sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f \ - --hash=sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d \ - --hash=sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127 \ - --hash=sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8 \ - --hash=sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f \ - --hash=sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580 \ - --hash=sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6 \ - --hash=sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691 \ - --hash=sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87 \ - --hash=sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd \ - --hash=sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96 \ - --hash=sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687 \ - --hash=sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33 \ - --hash=sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69 \ - --hash=sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653 \ - --hash=sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78 \ - --hash=sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261 \ - --hash=sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f \ - --hash=sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9 \ - --hash=sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d \ - --hash=sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737 \ - --hash=sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5 \ - --hash=sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0 +pydantic==2.5.2 \ + --hash=sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0 \ + --hash=sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd # via # -r requirements/main.in # fastapi + # pydantic-settings # safir +pydantic-core==2.14.5 \ + --hash=sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b \ + --hash=sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b \ + --hash=sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d \ + --hash=sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8 \ + --hash=sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124 \ + --hash=sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189 \ + --hash=sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c \ + --hash=sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d \ + --hash=sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f \ + --hash=sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520 \ + --hash=sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4 \ + --hash=sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6 \ + --hash=sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955 \ + --hash=sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3 \ + --hash=sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b \ + --hash=sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a \ + --hash=sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68 \ + --hash=sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3 \ + --hash=sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd \ + --hash=sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de \ + --hash=sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b \ + --hash=sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634 \ + --hash=sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7 \ + --hash=sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459 \ + --hash=sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7 \ + --hash=sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3 \ + --hash=sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331 \ + --hash=sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf \ + --hash=sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d \ + --hash=sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36 \ + --hash=sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59 \ + --hash=sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937 \ + --hash=sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc \ + --hash=sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093 \ + --hash=sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753 \ + --hash=sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706 \ + --hash=sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca \ + --hash=sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260 \ + --hash=sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997 \ + --hash=sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588 \ + --hash=sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71 \ + --hash=sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb \ + --hash=sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e \ + --hash=sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69 \ + --hash=sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5 \ + --hash=sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07 \ + --hash=sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1 \ + --hash=sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0 \ + --hash=sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd \ + --hash=sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8 \ + --hash=sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944 \ + --hash=sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26 \ + --hash=sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda \ + --hash=sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4 \ + --hash=sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9 \ + --hash=sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00 \ + --hash=sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe \ + --hash=sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6 \ + --hash=sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada \ + --hash=sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4 \ + --hash=sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7 \ + --hash=sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325 \ + --hash=sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4 \ + --hash=sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b \ + --hash=sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88 \ + --hash=sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04 \ + --hash=sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863 \ + --hash=sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0 \ + --hash=sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911 \ + --hash=sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b \ + --hash=sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e \ + --hash=sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144 \ + --hash=sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5 \ + --hash=sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720 \ + --hash=sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab \ + --hash=sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d \ + --hash=sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789 \ + --hash=sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec \ + --hash=sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2 \ + --hash=sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db \ + --hash=sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f \ + --hash=sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef \ + --hash=sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3 \ + --hash=sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209 \ + --hash=sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc \ + --hash=sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651 \ + --hash=sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8 \ + --hash=sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e \ + --hash=sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66 \ + --hash=sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7 \ + --hash=sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550 \ + --hash=sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd \ + --hash=sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405 \ + --hash=sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27 \ + --hash=sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093 \ + --hash=sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077 \ + --hash=sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113 \ + --hash=sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3 \ + --hash=sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6 \ + --hash=sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf \ + --hash=sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed \ + --hash=sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88 \ + --hash=sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe \ + --hash=sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18 \ + --hash=sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867 + # via pydantic +pydantic-settings==2.1.0 \ + --hash=sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c \ + --hash=sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a + # via -r requirements/main.in pyerfa==2.0.1.1 \ --hash=sha256:08b5abb90b34e819c1fca69047a76c0d344cb0c8fe4f7c8773f032d8afd623b4 \ --hash=sha256:0e95cf3d11f76f473bf011980e9ea367ca7e68ca675d8b32499814fb6e387d4c \ @@ -498,7 +580,9 @@ pyjwt[crypto]==2.8.0 \ python-dotenv==1.0.0 \ --hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \ --hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a - # via uvicorn + # via + # pydantic-settings + # uvicorn pyvo==1.4.2 \ --hash=sha256:66fe298865acfd725bc5f18750772d7f1b8899b8e5bb1775721520bed57d95cb \ --hash=sha256:74142901862b2c70b2eb0505355fe4ce4cf6907d290bb2a67b4f80c087b7a217 @@ -562,9 +646,9 @@ requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 # via pyvo -safir==4.5.0 \ - --hash=sha256:19268a22f9e530a98a780e416e4a8c79b40e275853fdae031d15f8f99fe7ebf4 \ - --hash=sha256:36301d094f4da08f1f54a3c7379db6603fa04e383df27a84a54c8a0a7a6cdf6e +safir==5.1.0 \ + --hash=sha256:0e4162b3b1fca558b037c06d7221b96996d7a55c92108e2e28e744d224c0076d \ + --hash=sha256:e04019e7e914aefc5ce1a9ca73c227eb3a84255d952bcd4cf3746e11cf7b1a15 # via -r requirements/main.in shortuuid==1.0.11 \ --hash=sha256:27ea8f28b1bd0bf8f15057a3ece57275d2059d2b0bb02854f02189962c13b6aa \ @@ -599,6 +683,7 @@ typing-extensions==4.9.0 \ # via # fastapi # pydantic + # pydantic-core uritemplate==4.1.1 \ --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e diff --git a/src/mobu/config.py b/src/mobu/config.py index 86f130a0..3ab3fe22 100644 --- a/src/mobu/config.py +++ b/src/mobu/config.py @@ -4,7 +4,8 @@ from pathlib import Path -from pydantic import BaseSettings, Field, HttpUrl +from pydantic import Field, HttpUrl +from pydantic_settings import BaseSettings from safir.logging import LogLevel, Profile __all__ = [ @@ -23,8 +24,8 @@ class Configuration(BaseSettings): "An https URL, which should be considered secret. If not set or" " set to `None`, this feature will be disabled." ), - env="ALERT_HOOK", - example="https://slack.example.com/ADFAW1452DAF41/", + validation_alias="MOBU_ALERT_HOOK", + examples=["https://slack.example.com/ADFAW1452DAF41/"], ) autostart: Path | None = Field( @@ -35,8 +36,8 @@ class Configuration(BaseSettings): " specifications. All flocks given there will be automatically" " started when mobu starts." ), - env="AUTOSTART", - example="/etc/mobu/autostart.yaml", + validation_alias="MOBU_AUTOSTART_PATH", + examples=["/etc/mobu/autostart.yaml"], ) environment_url: HttpUrl | None = Field( @@ -48,45 +49,45 @@ class Configuration(BaseSettings): " suite easier. If it is not set to a valid URL, mobu will abort" " during startup." ), - env="ENVIRONMENT_URL", - example="https://data.example.org/", + validation_alias="MOBU_ENVIRONMENT_URL", + examples=["https://data.example.org/"], ) gafaelfawr_token: str | None = Field( None, - field="Gafaelfawr admin token used to create user tokens", + title="Gafaelfawr admin token", description=( "This token is used to make an admin API call to Gafaelfawr to" " get a token for the user. This is only optional to make writing" " tests easier. mobu will abort during startup if it is not set." ), - env="GAFAELFAWR_TOKEN", - example="gt-vilSCi1ifK_MyuaQgMD2dQ.d6SIJhowv5Hs3GvujOyUig", + validation_alias="MOBU_GAFAELFAWR_TOKEN", + examples=["gt-vilSCi1ifK_MyuaQgMD2dQ.d6SIJhowv5Hs3GvujOyUig"], ) name: str = Field( "mobu", title="Name of application", description="Doubles as the root HTTP endpoint path.", - env="SAFIR_NAME", + validation_alias="MOBU_NAME", ) path_prefix: str = Field( "/mobu", title="URL prefix for application API", - env="SAFIR_PATH_PREFIX", + validation_alias="MOBU_PATH_PREFIX", ) profile: Profile = Field( Profile.development, title="Application logging profile", - env="SAFIR_PROFILE", + validation_alias="MOBU_LOGGING_PROFILE", ) log_level: LogLevel = Field( LogLevel.INFO, title="Log level of the application's logger", - env="SAFIR_LOG_LEVEL", + validation_alias="MOBU_LOG_LEVEL", ) diff --git a/src/mobu/factory.py b/src/mobu/factory.py index 0b807965..bc2b5bb9 100644 --- a/src/mobu/factory.py +++ b/src/mobu/factory.py @@ -76,9 +76,9 @@ def create_slack_webhook_client(self) -> SlackWebhookClient | None: Newly-created Slack client, or `None` if Slack alerting is not configured. """ - if not config.alert_hook or config.alert_hook == "None": + if not config.alert_hook: return None - return SlackWebhookClient(config.alert_hook, "Mobu", self._logger) + return SlackWebhookClient(str(config.alert_hook), "Mobu", self._logger) def create_solitary(self, solitary_config: SolitaryConfig) -> Solitary: """Create a runner for a solitary monkey. diff --git a/src/mobu/handlers/external.py b/src/mobu/handlers/external.py index e85840b7..688205f7 100644 --- a/src/mobu/handlers/external.py +++ b/src/mobu/handlers/external.py @@ -80,7 +80,7 @@ async def put_flock( context.logger.info( "Creating flock", flock=flock_config.name, - config=flock_config.dict(exclude_unset=True), + config=flock_config.model_dump(exclude_unset=True), ) flock = await context.manager.start_flock(flock_config) flock_url = context.request.url_for("get_flock", flock=flock.name) @@ -216,7 +216,7 @@ async def put_run( ) -> SolitaryResult: context.logger.info( "Running solitary monkey", - config=solitary_config.dict(exclude_unset=True), + config=solitary_config.model_dump(exclude_unset=True), ) solitary = context.factory.create_solitary(solitary_config) return await solitary.run() diff --git a/src/mobu/main.py b/src/mobu/main.py index 706eff44..65e0e927 100644 --- a/src/mobu/main.py +++ b/src/mobu/main.py @@ -34,9 +34,9 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: """Set up and tear down the the application.""" if not config.environment_url: - raise RuntimeError("ENVIRONMENT_URL was not set") + raise RuntimeError("MOBU_ENVIRONMENT_URL was not set") if not config.gafaelfawr_token: - raise RuntimeError("GAFAELFAWR_TOKEN was not set") + raise RuntimeError("MOBU_GAFAELFAWR_TOKEN was not set") await context_dependency.initialize() await context_dependency.process_context.manager.autostart() app.state.periodic_status = schedule_periodic(post_status, 60 * 60 * 24) @@ -74,7 +74,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # Enable Slack alerting for uncaught exceptions. if config.alert_hook: logger = structlog.get_logger("mobu") - SlackRouteErrorHandler.initialize(config.alert_hook, "mobu", logger) + SlackRouteErrorHandler.initialize(str(config.alert_hook), "mobu", logger) logger.debug("Initialized Slack webhook") # Enable the generic exception handler for client errors. diff --git a/src/mobu/models/business/base.py b/src/mobu/models/business/base.py index 59ab41e9..e1d9d230 100644 --- a/src/mobu/models/business/base.py +++ b/src/mobu/models/business/base.py @@ -1,6 +1,6 @@ """Base models for monkey business.""" -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from ..timings import StopwatchData @@ -17,7 +17,7 @@ class BusinessOptions(BaseModel): error_idle_time: int = Field( 60, title="How long to wait after an error before restarting", - example=600, + examples=[600], ) idle_time: int = Field( @@ -27,11 +27,10 @@ class BusinessOptions(BaseModel): "AFter each loop executing monkey business, the monkey will" " pause for this long in seconds" ), - example=60, + examples=[60], ) - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class BusinessConfig(BaseModel): @@ -50,9 +49,11 @@ class BusinessConfig(BaseModel): ) restart: bool = Field( - False, title="Restart business after failure", example=True + False, title="Restart business after failure", examples=[True] ) + model_config = ConfigDict(extra="forbid") + class BusinessData(BaseModel): """Status of a running business. @@ -61,13 +62,12 @@ class BusinessData(BaseModel): inheriting from this type and adding that information. """ - name: str = Field(..., title="Type of business", example="Business") + name: str = Field(..., title="Type of business", examples=["Business"]) - failure_count: int = Field(..., title="Number of failures", example=0) + failure_count: int = Field(..., title="Number of failures", examples=[0]) - success_count: int = Field(..., title="Number of successes", example=25) + success_count: int = Field(..., title="Number of successes", examples=[25]) timings: list[StopwatchData] = Field(..., title="Timings of events") - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") diff --git a/src/mobu/models/business/jupyterpythonloop.py b/src/mobu/models/business/jupyterpythonloop.py index bce9d62e..ec36aa35 100644 --- a/src/mobu/models/business/jupyterpythonloop.py +++ b/src/mobu/models/business/jupyterpythonloop.py @@ -21,7 +21,7 @@ class JupyterPythonLoopOptions(NubladoBusinessOptions): code: str = Field( 'print(2+2, end="")', title="Python code to execute", - example='print(2+2, end="")', + examples=['print(2+2, end="")'], ) max_executions: int = Field( @@ -30,7 +30,7 @@ class JupyterPythonLoopOptions(NubladoBusinessOptions): description=( "The number of code snippets to execute before restarting the lab." ), - example=25, + examples=[25], ge=1, ) diff --git a/src/mobu/models/business/notebookrunner.py b/src/mobu/models/business/notebookrunner.py index edd45102..c0f52143 100644 --- a/src/mobu/models/business/notebookrunner.py +++ b/src/mobu/models/business/notebookrunner.py @@ -32,7 +32,7 @@ class NotebookRunnerOptions(NubladoBusinessOptions): " lab, controlled by `delete_lab`), and then picks up where it" " left off." ), - example=25, + examples=[25], ge=1, ) @@ -69,12 +69,12 @@ class NotebookRunnerData(NubladoBusinessData): None, title="Name of the currently running notebook", description="Will not be present if no notebook is being executed", - example="cluster.ipynb", + examples=["cluster.ipynb"], ) running_code: str | None = Field( None, title="Currently running code", description="Will not be present if no code is being executed", - example='import json\nprint(json.dumps({"foo": "bar"})\n', + examples=['import json\nprint(json.dumps({"foo": "bar"})\n'], ) diff --git a/src/mobu/models/business/nublado.py b/src/mobu/models/business/nublado.py index 9dd7a0c8..7d447f70 100644 --- a/src/mobu/models/business/nublado.py +++ b/src/mobu/models/business/nublado.py @@ -151,7 +151,7 @@ class NubladoBusinessOptions(BusinessOptions): cachemachine_image_policy: CachemachinePolicy = Field( CachemachinePolicy.available, - field="Class of cachemachine images to use", + title="Class of cachemachine images to use", description=( "Whether to use the images available on all nodes, or the images" " desired by cachemachine. In instances where image streaming is" @@ -159,7 +159,7 @@ class NubladoBusinessOptions(BusinessOptions): " The default is ``available``. Only used if ``use_cachemachine``" " is true." ), - example=CachemachinePolicy.desired, + examples=[CachemachinePolicy.desired], ) delete_lab: bool = Field( @@ -170,18 +170,18 @@ class NubladoBusinessOptions(BusinessOptions): " iteration of monkey business involving JupyterLab. Set this" " to False to keep the same lab." ), - example=True, + examples=[True], ) delete_timeout: int = Field( - 60, title="Timeout for deleting a lab in seconds", example=60 + 60, title="Timeout for deleting a lab in seconds", examples=[60] ) execution_idle_time: int = Field( 1, title="How long to wait between cell executions in seconds", description="Used by JupyterPythonLoop and NotebookRunner", - example=1, + examples=[1], ) get_node: bool = Field( @@ -211,7 +211,7 @@ class NubladoBusinessOptions(BusinessOptions): " execution sequence out more realistically and avoid a thundering" " herd problem." ), - example=60, + examples=[60], ) jupyter_timeout: int = Field( @@ -242,29 +242,29 @@ class NubladoBusinessOptions(BusinessOptions): " queries prior to starting the spawn will fail with an exception" " that closes the progress EventStream." ), - example=10, + examples=[10], ) spawn_timeout: int = Field( - 610, title="Timeout for spawning a lab in seconds", example=610 + 610, title="Timeout for spawning a lab in seconds", examples=[610] ) url_prefix: str = Field("/nb/", title="URL prefix for JupyterHub") use_cachemachine: bool = Field( True, - field="Whether to use cachemachine to look up an image", + title="Whether to use cachemachine to look up an image", description=( "Set this to false in environments using the new Nublado lab" " controller." ), - example=False, + examples=[False], ) working_directory: str | None = Field( None, title="Working directory when running code", - example="notebooks/tutorial-notebooks", + examples=["notebooks/tutorial-notebooks"], ) diff --git a/src/mobu/models/business/tap.py b/src/mobu/models/business/tap.py index d657d896..cd159d3d 100644 --- a/src/mobu/models/business/tap.py +++ b/src/mobu/models/business/tap.py @@ -22,7 +22,7 @@ class TAPBusinessOptions(BusinessOptions): "By default, queries to TAP are run via the sync endpoint." " Set this to false to run as an async query." ), - example=True, + examples=[True], ) diff --git a/src/mobu/models/business/tapqueryrunner.py b/src/mobu/models/business/tapqueryrunner.py index 465d3970..64f2da1c 100644 --- a/src/mobu/models/business/tapqueryrunner.py +++ b/src/mobu/models/business/tapqueryrunner.py @@ -22,9 +22,11 @@ class TAPQueryRunnerOptions(TAPBusinessOptions): ..., title="TAP queries", description="List of queries to be run", - example=[ - "SELECT TOP 10 * FROM TAP_SCHEMA.schemas", - "SELECT TOP 10 * FROM MYDB.MyTable", + examples=[ + [ + "SELECT TOP 10 * FROM TAP_SCHEMA.schemas", + "SELECT TOP 10 * FROM MYDB.MyTable", + ] ], ) diff --git a/src/mobu/models/business/tapquerysetrunner.py b/src/mobu/models/business/tapquerysetrunner.py index 88b8d174..eb745ab9 100644 --- a/src/mobu/models/business/tapquerysetrunner.py +++ b/src/mobu/models/business/tapquerysetrunner.py @@ -21,7 +21,7 @@ class TAPQuerySetRunnerOptions(TAPBusinessOptions): query_set: str = Field( "dp0.1", title="Which query template set to use for a TapQueryRunner", - example="dp0.2", + examples=["dp0.2"], ) diff --git a/src/mobu/models/flock.py b/src/mobu/models/flock.py index be4b6aee..562aa9da 100644 --- a/src/mobu/models/flock.py +++ b/src/mobu/models/flock.py @@ -1,9 +1,9 @@ """Models for a collection of monkeys.""" from datetime import datetime -from typing import Any +from typing import Self -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, model_validator from .business.empty import EmptyLoopConfig from .business.jupyterpythonloop import JupyterPythonLoopConfig @@ -21,9 +21,9 @@ class FlockConfig(BaseModel): number of individual monkeys, which will all run as different users. """ - name: str = Field(..., title="Name of the flock", example="autostart") + name: str = Field(..., title="Name of the flock", examples=["autostart"]) - count: int = Field(..., title="How many monkeys to run", example=100) + count: int = Field(..., title="How many monkeys to run", examples=[100]) users: list[User] | None = Field( None, @@ -45,7 +45,7 @@ class FlockConfig(BaseModel): ..., title="Token scopes", description="Must include all scopes required to run the business", - example=["exec:notebook", "read:tap"], + examples=[["exec:notebook", "read:tap"]], ) business: ( @@ -56,32 +56,21 @@ class FlockConfig(BaseModel): | EmptyLoopConfig ) = Field(..., title="Business to run") - @validator("users") - def _valid_users( - cls, v: list[User] | None, values: dict[str, Any] - ) -> list[User] | None: - if v is None: - return v - if "count" in values and len(v) != values["count"]: - count = values["count"] - raise ValueError(f"users list must contain {count} elements") - return v - - @validator("user_spec", always=True) - def _valid_user_spec( - cls, v: UserSpec | None, values: dict[str, Any] - ) -> UserSpec | None: - if v is None and ("users" not in values or values["users"] is None): + @model_validator(mode="after") + def _validate(self) -> Self: + if not self.users and not self.user_spec: raise ValueError("one of users or user_spec must be provided") - if v and "users" in values and values["users"]: + if self.users and self.user_spec: raise ValueError("both users and user_spec provided") - return v + if self.count and self.users and len(self.users) != self.count: + raise ValueError(f"users list must contain {self.count} elements") + return self class FlockData(BaseModel): """Information about a running flock.""" - name: str = Field(..., title="Name of the flock", example="autostart") + name: str = Field(..., title="Name of the flock", examples=["autostart"]) config: FlockConfig = Field(..., title="Configuration for the flock") @@ -91,29 +80,29 @@ class FlockData(BaseModel): class FlockSummary(BaseModel): """Summary statistics about a running flock.""" - name: str = Field(..., title="Name of the flock", example="autostart") + name: str = Field(..., title="Name of the flock", examples=["autostart"]) business: str = Field( ..., title="Name of the business the flock is running", - example="NotebookRunner", + examples=["NotebookRunner"], ) start_time: datetime | None = Field( ..., title="When the flock was started", description="Will be null if the flock hasn't started", - example="2021-07-21T19:43:40.446072+00:00", + examples=["2021-07-21T19:43:40.446072+00:00"], ) monkey_count: int = Field( - ..., title="Number of monkeys in the flock", example=5 + ..., title="Number of monkeys in the flock", examples=[5] ) success_count: int = Field( - ..., title="Total number of monkey successes in flock", example=455 + ..., title="Total number of monkey successes in flock", examples=[455] ) failure_count: int = Field( - ..., title="Total number of monkey failures in flock", example=4 + ..., title="Total number of monkey failures in flock", examples=[4] ) diff --git a/src/mobu/models/monkey.py b/src/mobu/models/monkey.py index 5767d7d5..18ba4ea0 100644 --- a/src/mobu/models/monkey.py +++ b/src/mobu/models/monkey.py @@ -27,7 +27,7 @@ class MonkeyData(BaseModel): name: str = Field(..., title="Name of the monkey") state: MonkeyState = Field( - ..., title="State of monkey", example=MonkeyState.RUNNING + ..., title="State of monkey", examples=[MonkeyState.RUNNING] ) user: AuthenticatedUser = Field( diff --git a/src/mobu/models/solitary.py b/src/mobu/models/solitary.py index 74fbef53..64233659 100644 --- a/src/mobu/models/solitary.py +++ b/src/mobu/models/solitary.py @@ -24,7 +24,7 @@ class SolitaryConfig(BaseModel): ..., title="Token scopes", description="Must include all scopes required to run the business", - example=["exec:notebook", "read:tap"], + examples=[["exec:notebook", "read:tap"]], ) business: ( diff --git a/src/mobu/models/timings.py b/src/mobu/models/timings.py index 08028425..a86b5019 100644 --- a/src/mobu/models/timings.py +++ b/src/mobu/models/timings.py @@ -8,32 +8,34 @@ class StopwatchData(BaseModel): """Timing for a single event.""" - event: str = Field(..., title="Name of the event", example="lab_create") + event: str = Field(..., title="Name of the event", examples=["lab_create"]) annotations: dict[str, str] = Field( default_factory=dict, title="Event annotations", - example={"notebook": "example.ipynb"}, + examples=[{"notebook": "example.ipynb"}], ) start: datetime = Field( - ..., title="Start of event", example="2021-07-21T19:43:40.446072+00:00" + ..., + title="Start of event", + examples=["2021-07-21T19:43:40.446072+00:00"], ) stop: datetime | None = Field( None, title="End of event", description="Will be null if the event is ongoing", - example="2021-07-21T19:43:40.514623+00:00", + examples=["2021-07-21T19:43:40.514623+00:00"], ) elapsed: float | None = Field( None, title="Duration of event in seconds", description="Will be null if the event is ongoing", - example=0.068551, + examples=[0.068551], ) failed: bool = Field( - False, title="Whether the event failed", example=False + False, title="Whether the event failed", examples=[False] ) diff --git a/src/mobu/models/user.py b/src/mobu/models/user.py index fca48a31..d5cede55 100644 --- a/src/mobu/models/user.py +++ b/src/mobu/models/user.py @@ -14,7 +14,7 @@ class User(BaseModel): """Configuration for the user whose credentials the monkey will use.""" - username: str = Field(..., title="Username", example="testuser") + username: str = Field(..., title="Username", examples=["testuser"]) uidnumber: int | None = Field( None, @@ -23,7 +23,7 @@ class User(BaseModel): "If omitted, Gafaelfawr will assign a UID. (Gafaelfawr UID" " assignment requires Firestore be configured.)" ), - example=60001, + examples=[60001], ) gidnumber: int | None = Field( @@ -35,7 +35,7 @@ class User(BaseModel): " (Gafaelfawr UID and GID assignment requires Firestore and" " synthetic user private groups to be configured.)" ), - example=60001, + examples=[60001], ) @@ -46,7 +46,7 @@ class UserSpec(BaseModel): ..., title="Prefix for usernames", description="Each user will be formed by appending a number to this", - example="lsptestuser", + examples=["lsptestuser"], ) uid_start: int | None = Field( @@ -57,7 +57,7 @@ class UserSpec(BaseModel): " omitted, Gafaelfawr will assign UIDs. (Gafaelfawr UID assignment" " requires Firestore be configured.)" ), - example=60000, + examples=[60000], ) gid_start: int | None = Field( @@ -70,7 +70,7 @@ class UserSpec(BaseModel): " (which requires Firestore and synthetic user private groups to" " be configured)." ), - example=60000, + examples=[60000], ) @@ -80,11 +80,11 @@ class AuthenticatedUser(User): scopes: list[str] = Field( ..., title="Token scopes", - example=["exec:notebook", "read:tap"], + examples=[["exec:notebook", "read:tap"]], ) token: str = Field( ..., title="Authentication token for user", - example="gt-1PhgAeB-9Fsa-N1NhuTu_w.oRvMvAQp1bWfx8KCJKNohg", + examples=["gt-1PhgAeB-9Fsa-N1NhuTu_w.oRvMvAQp1bWfx8KCJKNohg"], ) diff --git a/src/mobu/services/business/notebookrunner.py b/src/mobu/services/business/notebookrunner.py index 2b0412bf..d18e976b 100644 --- a/src/mobu/services/business/notebookrunner.py +++ b/src/mobu/services/business/notebookrunner.py @@ -170,5 +170,5 @@ def dump(self) -> NotebookRunnerData: return NotebookRunnerData( notebook=self._notebook.name if self._notebook else None, running_code=self._running_code, - **super().dump().dict(), + **super().dump().model_dump(), ) diff --git a/src/mobu/services/business/nublado.py b/src/mobu/services/business/nublado.py index 72a7a5f4..10408d3c 100644 --- a/src/mobu/services/business/nublado.py +++ b/src/mobu/services/business/nublado.py @@ -331,4 +331,6 @@ async def delete_lab(self) -> None: self._image = None def dump(self) -> NubladoBusinessData: - return NubladoBusinessData(image=self._image, **super().dump().dict()) + return NubladoBusinessData( + image=self._image, **super().dump().model_dump() + ) diff --git a/src/mobu/services/business/tap.py b/src/mobu/services/business/tap.py index 450b94c6..00518667 100644 --- a/src/mobu/services/business/tap.py +++ b/src/mobu/services/business/tap.py @@ -127,7 +127,7 @@ async def run_sync_query(self, query: str) -> None: def dump(self) -> TAPBusinessData: return TAPBusinessData( - running_query=self._running_query, **super().dump().dict() + running_query=self._running_query, **super().dump().model_dump() ) def _make_client(self, token: str) -> pyvo.dal.TAPService: diff --git a/src/mobu/services/manager.py b/src/mobu/services/manager.py index f32406f0..25ce5d94 100644 --- a/src/mobu/services/manager.py +++ b/src/mobu/services/manager.py @@ -63,7 +63,9 @@ async def autostart(self) -> None: return with config.autostart.open("r") as f: autostart = yaml.safe_load(f) - flock_configs = [FlockConfig.parse_obj(flock) for flock in autostart] + flock_configs = [ + FlockConfig.model_validate(flock) for flock in autostart + ] for flock_config in flock_configs: await self.start_flock(flock_config) diff --git a/src/mobu/services/monkey.py b/src/mobu/services/monkey.py index 144b4e39..08f65dfb 100644 --- a/src/mobu/services/monkey.py +++ b/src/mobu/services/monkey.py @@ -110,9 +110,9 @@ def __init__( raise TypeError(msg) self._slack = None - if config.alert_hook and config.alert_hook != "None": + if config.alert_hook: self._slack = SlackWebhookClient( - config.alert_hook, "Mobu", self._global_logger + str(config.alert_hook), "Mobu", self._global_logger ) async def alert(self, exc: Exception) -> None: diff --git a/src/mobu/status.py b/src/mobu/status.py index aa6a00d5..ede95035 100644 --- a/src/mobu/status.py +++ b/src/mobu/status.py @@ -29,7 +29,7 @@ async def post_status() -> None: flock_plural = "flock" if flock_count == 1 else "flocks" text = ( f"Currently running {flock_count} {flock_plural} against" - f" {config.environment_url}:\n" + f' {str(config.environment_url).rstrip("/")}:\n' ) for summary in summaries: if summary.start_time: diff --git a/src/mobu/storage/cachemachine.py b/src/mobu/storage/cachemachine.py index a44c3cb4..152fb8aa 100644 --- a/src/mobu/storage/cachemachine.py +++ b/src/mobu/storage/cachemachine.py @@ -22,23 +22,23 @@ class JupyterCachemachineImage(BaseModel): reference: str = Field( ..., title="Docker reference for the image", - example="registry.hub.docker.com/lsstsqre/sciplat-lab:w_2021_13", + examples=["registry.hub.docker.com/lsstsqre/sciplat-lab:w_2021_13"], ) name: str = Field( ..., title="Human-readable name for the image", - example="Weekly 2021_34", + examples=["Weekly 2021_34"], ) digest: str | None = Field( ..., title="Hash of the last layer of the Docker container", description="May be null if the digest isn't known", - example=( + examples=[ "sha256:419c4b7e14603711b25fa9e0569460a753c4b2449fe275bb5f89743b" "01794a30" - ), + ], ) def __str__(self) -> str: diff --git a/src/mobu/storage/gafaelfawr.py b/src/mobu/storage/gafaelfawr.py index a8c623f3..8729ab89 100644 --- a/src/mobu/storage/gafaelfawr.py +++ b/src/mobu/storage/gafaelfawr.py @@ -41,7 +41,7 @@ class _AdminTokenRequest(BaseModel): """ username: str = Field( - ..., min_length=1, max_length=64, regex=USERNAME_REGEX + ..., min_length=1, max_length=64, pattern=USERNAME_REGEX ) token_type: _TokenType = Field(...) scopes: list[str] = Field([]) @@ -132,10 +132,10 @@ async def create_service_token( r = await self._client.post( self._token_url, headers={"Authorization": f"Bearer {config.gafaelfawr_token}"}, - json=json.loads(request.json(exclude_none=True)), + json=json.loads(request.model_dump_json(exclude_none=True)), ) r.raise_for_status() - token = _NewToken.parse_obj(r.json()) + token = _NewToken.model_validate(r.json()) return AuthenticatedUser( username=user.username, uidnumber=request.uid, diff --git a/tests/conftest.py b/tests/conftest.py index 89cdbad6..5359ecf8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,6 @@ from fastapi import FastAPI from httpx import AsyncClient from pydantic import HttpUrl -from pydantic.tools import parse_obj_as from safir.testing.slack import MockSlackWebhook, mock_slack_webhook from mobu import main @@ -41,7 +40,7 @@ def _configure() -> Iterator[None]: minimal test configuration and a unique admin token that is replaced after the test runs. """ - config.environment_url = parse_obj_as(HttpUrl, "https://test.example.com") + config.environment_url = HttpUrl("https://test.example.com") config.gafaelfawr_token = make_gafaelfawr_token() yield config.environment_url = None @@ -108,6 +107,6 @@ async def mock_connect( @pytest.fixture def slack(respx_mock: respx.Router) -> Iterator[MockSlackWebhook]: - config.alert_hook = parse_obj_as(HttpUrl, "https://slack.example.com/XXXX") + config.alert_hook = HttpUrl("https://slack.example.com/XXXX") yield mock_slack_webhook(str(config.alert_hook), respx_mock) config.alert_hook = None diff --git a/tests/handlers/flock_test.py b/tests/handlers/flock_test.py index cc6feedb..922a2e86 100644 --- a/tests/handlers/flock_test.py +++ b/tests/handlers/flock_test.py @@ -144,15 +144,8 @@ async def test_user_list( "name": "test", "count": 2, "users": [ - { - "username": "testuser", - "uidnumber": 1000, - "gidnumber": 1056, - }, - { - "username": "otheruser", - "uidnumber": 60000, - }, + {"username": "testuser", "uidnumber": 1000, "gidnumber": 1056}, + {"username": "otheruser", "uidnumber": 60000}, ], "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, @@ -225,14 +218,8 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: "name": "test", "count": 2, "users": [ - { - "username": "testuser", - "uidnumber": 1000, - }, - { - "username": "otheruser", - "uidnumber": 60000, - }, + {"username": "testuser", "uidnumber": 1000}, + {"username": "otheruser", "uidnumber": 60000}, ], "user_spec": {"username_prefix": "testuser", "uid_start": 1000}, "scopes": [], @@ -243,9 +230,12 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.json() == { "detail": [ { - "loc": ["body", "user_spec"], - "msg": "both users and user_spec provided", + "ctx": ANY, + "input": ANY, + "loc": ["body"], + "msg": "Value error, both users and user_spec provided", "type": "value_error", + "url": ANY, } ] } @@ -264,9 +254,14 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.json() == { "detail": [ { - "loc": ["body", "user_spec"], - "msg": "one of users or user_spec must be provided", + "ctx": ANY, + "input": ANY, + "loc": ["body"], + "msg": ( + "Value error, one of users or user_spec must be provided" + ), "type": "value_error", + "url": ANY, } ] } @@ -278,18 +273,9 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: "name": "test", "count": 2, "users": [ - { - "username": "testuser", - "uidnumber": 1000, - }, - { - "username": "otheruser", - "uidnumber": 60000, - }, - { - "username": "thirduser", - "uidnumber": 70000, - }, + {"username": "testuser", "uidnumber": 1000}, + {"username": "otheruser", "uidnumber": 60000}, + {"username": "thirduser", "uidnumber": 70000}, ], "scopes": [], "business": {"type": "EmptyLoop"}, @@ -299,15 +285,13 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.json() == { "detail": [ { - "loc": ["body", "users"], - "msg": "users list must contain 2 elements", + "ctx": ANY, + "input": ANY, + "loc": ["body"], + "msg": "Value error, users list must contain 2 elements", "type": "value_error", - }, - { - "loc": ["body", "user_spec"], - "msg": "one of users or user_spec must be provided", - "type": "value_error", - }, + "url": ANY, + } ] } @@ -317,12 +301,7 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: json={ "name": "test", "count": 2, - "users": [ - { - "username": "testuser", - "uidnumber": 1000, - } - ], + "users": [{"username": "testuser", "uidnumber": 1000}], "scopes": [], "business": {"type": "EmptyLoop"}, }, @@ -331,14 +310,12 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.json() == { "detail": [ { - "loc": ["body", "users"], - "msg": "users list must contain 2 elements", - "type": "value_error", - }, - { - "loc": ["body", "user_spec"], - "msg": "one of users or user_spec must be provided", + "ctx": ANY, + "input": ANY, + "loc": ["body"], + "msg": "Value error, users list must contain 2 elements", "type": "value_error", + "url": ANY, }, ] } @@ -357,8 +334,10 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.status_code == 422 result = r.json() assert result["detail"][0] == { - "ctx": {"given": "UnknownBusiness", "permitted": [ANY]}, - "loc": ["body", "business", "type"], + "ctx": ANY, + "input": "UnknownBusiness", + "loc": ["body", "business", "TAPQueryRunnerConfig", "type"], "msg": ANY, - "type": "value_error.const", + "type": "literal_error", + "url": ANY, } diff --git a/tests/monkeyflocker_test.py b/tests/monkeyflocker_test.py index 4ae8aa59..cec9b5a0 100644 --- a/tests/monkeyflocker_test.py +++ b/tests/monkeyflocker_test.py @@ -39,8 +39,8 @@ def monkeyflocker_app(tmp_path: Path) -> Iterator[UvicornProcess]: working_directory=tmp_path, factory="tests.support.monkeyflocker:create_app", env={ - "ENVIRONMENT_URL": str(config.environment_url), - "GAFAELFAWR_TOKEN": config.gafaelfawr_token, + "MOBU_ENVIRONMENT_URL": str(config.environment_url), + "MOBU_GAFAELFAWR_TOKEN": config.gafaelfawr_token, }, ) yield uvicorn diff --git a/tests/support/cachemachine.py b/tests/support/cachemachine.py index aa2ad2fa..5ffb1778 100644 --- a/tests/support/cachemachine.py +++ b/tests/support/cachemachine.py @@ -2,6 +2,8 @@ from __future__ import annotations +from urllib.parse import urljoin + import respx from httpx import Request, Response @@ -85,6 +87,8 @@ def available(self, request: Request) -> Response: def mock_cachemachine(respx_mock: respx.Router) -> MockCachemachine: """Set up a mock cachemachine.""" mock = MockCachemachine() - url = f"{config.environment_url}/cachemachine/jupyter/available" + url = urljoin( + str(config.environment_url), "/cachemachine/jupyter/available" + ) respx_mock.get(url).mock(side_effect=mock.available) return mock