diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 660357f..c98b98b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,6 +50,7 @@ jobs: with: python-version: ${{ matrix.python }} tox-envs: "py,coverage-report,typing" + tox-requirements: "requirements/tox.txt" build: runs-on: ubuntu-latest diff --git a/.github/workflows/periodic-ci.yaml b/.github/workflows/periodic-ci.yaml index 60d4bbd..0d3cff5 100644 --- a/.github/workflows/periodic-ci.yaml +++ b/.github/workflows/periodic-ci.yaml @@ -37,6 +37,7 @@ jobs: with: python-version: ${{ matrix.python }} tox-envs: "lint,typing,py" + tox-requirements: "requirements/tox.txt" use-cache: false - name: Report status diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84e8e58..441d876 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.7 + rev: v0.4.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a1df04..0d0186d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,12 @@ Dependencies are updated to the latest available version during each release, an Find changes for the upcoming release in the project's [changelog.d directory](https://github.com/lsst-sqre/fastapi-bootcamp/tree/main/changelog.d/). + + +## 1.0.0 (2024-04-30) + +### New features + +- Add examples of FastAPI path operation functions to the external router. + +- Add `/fastapi-bootcamp/astroplan` router with a basic API for observational sites and computing the observability of targets from those sites. This API is build around [astroplan](https://astroplan.readthedocs.io/en/stable/). We're including it in this app to demonstrate the service architecture that we prefer in SQuaRE, where the application's domain is isolated from concerns of the web API and even storage and other types of external adapters. diff --git a/Makefile b/Makefile index cf6a0ce..acedfc9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: help help: - @echo "Make targets for fastapi-bootcamp" + @echo "Make targets for example" @echo "make init - Set up dev environment" @echo "make run - Start a local development instance" @echo "make update - Update pinned dependencies and run make init" @@ -10,10 +10,11 @@ help: .PHONY: init init: pip install --upgrade uv - uv pip install --upgrade pre-commit tox + uv pip install -r requirements/main.txt -r requirements/dev.txt \ + -r requirements/tox.txt uv pip install --editable . - uv pip install -r requirements/main.txt -r requirements/dev.txt rm -rf .tox + uv pip install --upgrade pre-commit pre-commit install .PHONY: run @@ -32,6 +33,8 @@ update-deps: --output-file requirements/main.txt requirements/main.in uv pip compile --upgrade --generate-hashes \ --output-file requirements/dev.txt requirements/dev.in + uv pip compile --upgrade --generate-hashes \ + --output-file requirements/tox.txt requirements/tox.in # Useful for testing against a Git version of Safir. .PHONY: update-deps-no-hashes @@ -41,3 +44,5 @@ update-deps-no-hashes: --output-file requirements/main.txt requirements/main.in uv pip compile --upgrade \ --output-file requirements/dev.txt requirements/dev.in + uv pip compile --upgrade \ + --output-file requirements/tox.txt requirements/tox.in diff --git a/changelog.d/20240429_155939_jsick_DM_43939.md b/changelog.d/20240429_155939_jsick_DM_43939.md deleted file mode 100644 index 2bbe747..0000000 --- a/changelog.d/20240429_155939_jsick_DM_43939.md +++ /dev/null @@ -1,3 +0,0 @@ -### New features - -- Add examples of FastAPI path operation functions to the external router. diff --git a/pyproject.toml b/pyproject.toml index cc32ffe..58e1d49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,7 @@ ignore = [ "TID252", # if we're going to use relative imports, use them always "TRY003", # good general advice but lint is way too aggressive "TRY301", # sometimes raising exceptions inside try is the best flow + "UP040", # type keyword not supported by mypy yet # The following settings should be disabled when using ruff format # per https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules diff --git a/requirements/dev.txt b/requirements/dev.txt index 88b3521..5146640 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -128,67 +128,67 @@ click-log==0.4.0 \ --hash=sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975 \ --hash=sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756 # via scriv -coverage==7.4.4 \ - --hash=sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c \ - --hash=sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63 \ - --hash=sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7 \ - --hash=sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f \ - --hash=sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8 \ - --hash=sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf \ - --hash=sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0 \ - --hash=sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384 \ - --hash=sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76 \ - --hash=sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7 \ - --hash=sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d \ - --hash=sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70 \ - --hash=sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f \ - --hash=sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818 \ - --hash=sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b \ - --hash=sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d \ - --hash=sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec \ - --hash=sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083 \ - --hash=sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2 \ - --hash=sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9 \ - --hash=sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd \ - --hash=sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade \ - --hash=sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e \ - --hash=sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a \ - --hash=sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227 \ - --hash=sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87 \ - --hash=sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c \ - --hash=sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e \ - --hash=sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c \ - --hash=sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e \ - --hash=sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd \ - --hash=sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec \ - --hash=sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562 \ - --hash=sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8 \ - --hash=sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677 \ - --hash=sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357 \ - --hash=sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c \ - --hash=sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd \ - --hash=sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49 \ - --hash=sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286 \ - --hash=sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1 \ - --hash=sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf \ - --hash=sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51 \ - --hash=sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409 \ - --hash=sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384 \ - --hash=sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e \ - --hash=sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978 \ - --hash=sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57 \ - --hash=sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e \ - --hash=sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2 \ - --hash=sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48 \ - --hash=sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4 +coverage==7.5.0 \ + --hash=sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0 \ + --hash=sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7 \ + --hash=sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631 \ + --hash=sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b \ + --hash=sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25 \ + --hash=sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7 \ + --hash=sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067 \ + --hash=sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b \ + --hash=sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46 \ + --hash=sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5 \ + --hash=sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a \ + --hash=sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3 \ + --hash=sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4 \ + --hash=sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1 \ + --hash=sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475 \ + --hash=sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c \ + --hash=sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1 \ + --hash=sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be \ + --hash=sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58 \ + --hash=sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de \ + --hash=sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a \ + --hash=sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb \ + --hash=sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95 \ + --hash=sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb \ + --hash=sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517 \ + --hash=sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656 \ + --hash=sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e \ + --hash=sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880 \ + --hash=sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9 \ + --hash=sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f \ + --hash=sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2 \ + --hash=sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af \ + --hash=sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2 \ + --hash=sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932 \ + --hash=sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc \ + --hash=sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d \ + --hash=sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d \ + --hash=sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493 \ + --hash=sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64 \ + --hash=sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4 \ + --hash=sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff \ + --hash=sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8 \ + --hash=sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1 \ + --hash=sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4 \ + --hash=sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e \ + --hash=sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375 \ + --hash=sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88 \ + --hash=sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a \ + --hash=sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb \ + --hash=sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4 \ + --hash=sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743 \ + --hash=sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9 # via pytest-cov distlib==0.3.8 \ --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 # via virtualenv -filelock==3.13.4 \ - --hash=sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f \ - --hash=sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4 +filelock==3.14.0 \ + --hash=sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f \ + --hash=sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a # via virtualenv h11==0.14.0 \ --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ @@ -201,9 +201,9 @@ httpcore==1.0.5 \ httpx==0.27.0 \ --hash=sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5 \ --hash=sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5 -identify==2.5.35 \ - --hash=sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791 \ - --hash=sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e +identify==2.5.36 \ + --hash=sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa \ + --hash=sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d # via pre-commit idna==3.7 \ --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ @@ -290,34 +290,34 @@ mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -mypy==1.9.0 \ - --hash=sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6 \ - --hash=sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913 \ - --hash=sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129 \ - --hash=sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc \ - --hash=sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974 \ - --hash=sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374 \ - --hash=sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150 \ - --hash=sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03 \ - --hash=sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9 \ - --hash=sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02 \ - --hash=sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89 \ - --hash=sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2 \ - --hash=sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d \ - --hash=sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3 \ - --hash=sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612 \ - --hash=sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e \ - --hash=sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3 \ - --hash=sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e \ - --hash=sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd \ - --hash=sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04 \ - --hash=sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed \ - --hash=sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185 \ - --hash=sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf \ - --hash=sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b \ - --hash=sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4 \ - --hash=sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f \ - --hash=sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6 +mypy==1.10.0 \ + --hash=sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061 \ + --hash=sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99 \ + --hash=sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de \ + --hash=sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a \ + --hash=sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9 \ + --hash=sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec \ + --hash=sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1 \ + --hash=sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131 \ + --hash=sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f \ + --hash=sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821 \ + --hash=sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5 \ + --hash=sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee \ + --hash=sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e \ + --hash=sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746 \ + --hash=sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2 \ + --hash=sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0 \ + --hash=sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b \ + --hash=sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53 \ + --hash=sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30 \ + --hash=sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda \ + --hash=sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051 \ + --hash=sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2 \ + --hash=sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7 \ + --hash=sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee \ + --hash=sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727 \ + --hash=sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976 \ + --hash=sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4 mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 @@ -330,104 +330,104 @@ packaging==24.0 \ --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 # via pytest -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 +platformdirs==4.2.1 \ + --hash=sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf \ + --hash=sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1 # via virtualenv -pluggy==1.4.0 \ - --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ - --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # via pytest pre-commit==3.7.0 \ --hash=sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab \ --hash=sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060 -pydantic==2.7.0 \ - --hash=sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352 \ - --hash=sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383 -pydantic-core==2.18.1 \ - --hash=sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6 \ - --hash=sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb \ - --hash=sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0 \ - --hash=sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6 \ - --hash=sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47 \ - --hash=sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a \ - --hash=sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a \ - --hash=sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac \ - --hash=sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88 \ - --hash=sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db \ - --hash=sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d \ - --hash=sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d \ - --hash=sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9 \ - --hash=sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e \ - --hash=sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b \ - --hash=sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d \ - --hash=sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649 \ - --hash=sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c \ - --hash=sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1 \ - --hash=sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09 \ - --hash=sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0 \ - --hash=sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90 \ - --hash=sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d \ - --hash=sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294 \ - --hash=sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144 \ - --hash=sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b \ - --hash=sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1 \ - --hash=sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b \ - --hash=sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2 \ - --hash=sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad \ - --hash=sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622 \ - --hash=sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17 \ - --hash=sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06 \ - --hash=sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc \ - --hash=sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50 \ - --hash=sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d \ - --hash=sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59 \ - --hash=sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539 \ - --hash=sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a \ - --hash=sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b \ - --hash=sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5 \ - --hash=sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9 \ - --hash=sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278 \ - --hash=sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6 \ - --hash=sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44 \ - --hash=sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0 \ - --hash=sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb \ - --hash=sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80 \ - --hash=sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5 \ - --hash=sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570 \ - --hash=sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b \ - --hash=sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de \ - --hash=sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6 \ - --hash=sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8 \ - --hash=sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203 \ - --hash=sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7 \ - --hash=sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048 \ - --hash=sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae \ - --hash=sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89 \ - --hash=sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f \ - --hash=sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926 \ - --hash=sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2 \ - --hash=sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76 \ - --hash=sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d \ - --hash=sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411 \ - --hash=sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9 \ - --hash=sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2 \ - --hash=sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586 \ - --hash=sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35 \ - --hash=sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c \ - --hash=sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143 \ - --hash=sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6 \ - --hash=sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60 \ - --hash=sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b \ - --hash=sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226 \ - --hash=sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519 \ - --hash=sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31 \ - --hash=sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7 \ - --hash=sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b +pydantic==2.7.1 \ + --hash=sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5 \ + --hash=sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc +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 # via pydantic -pytest==8.1.1 \ - --hash=sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7 \ - --hash=sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044 +pytest==8.2.0 \ + --hash=sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233 \ + --hash=sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f # via # pytest-asyncio # pytest-cov @@ -494,24 +494,24 @@ requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 # via scriv -ruff==0.3.7 \ - --hash=sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce \ - --hash=sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b \ - --hash=sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1 \ - --hash=sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb \ - --hash=sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b \ - --hash=sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74 \ - --hash=sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f \ - --hash=sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f \ - --hash=sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7 \ - --hash=sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f \ - --hash=sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51 \ - --hash=sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663 \ - --hash=sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2 \ - --hash=sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba \ - --hash=sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea \ - --hash=sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4 \ - --hash=sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a +ruff==0.4.2 \ + --hash=sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b \ + --hash=sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68 \ + --hash=sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac \ + --hash=sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483 \ + --hash=sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc \ + --hash=sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5 \ + --hash=sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798 \ + --hash=sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd \ + --hash=sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22 \ + --hash=sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376 \ + --hash=sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe \ + --hash=sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480 \ + --hash=sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e \ + --hash=sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa \ + --hash=sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5 \ + --hash=sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf \ + --hash=sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069 scriv==1.5.1 \ --hash=sha256:30ae9ff8d144f8e0cf394c4e1d379542f1b3823767642955b54ec40dc00b32b6 \ --hash=sha256:a3adc657733b4124fcb54527a5f3daab0d3c300de82d0fd2b9b297b243151b78 @@ -537,7 +537,7 @@ urllib3==2.2.1 \ --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 # via requests -virtualenv==20.25.3 \ - --hash=sha256:7bb554bbdfeaacc3349fa614ea5bff6ac300fc7c335e9facf3a3bcfc703f45be \ - --hash=sha256:8aac4332f2ea6ef519c648d0bc48a5b1d324994753519919bddbb1aff25a104e +virtualenv==20.26.1 \ + --hash=sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b \ + --hash=sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75 # via pre-commit diff --git a/requirements/main.in b/requirements/main.in index 985d2fe..77ebfa1 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -13,6 +13,8 @@ starlette uvicorn[standard] # Other dependencies. +astroplan pydantic pydantic-settings safir>=5 +python-slugify diff --git a/requirements/main.txt b/requirements/main.txt index a469c05..e07b26c 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -11,6 +11,43 @@ anyio==4.3.0 \ # httpx # starlette # watchfiles +astroplan==0.10 \ + --hash=sha256:1670c9c42143d62924e8a6e0287ec25935fcf985a8b928438bc4da0241f4875a +astropy==6.0.1 \ + --hash=sha256:034dff5994428fb89813f40a18600dd8804128c52edf3d1baa8936eca3738de4 \ + --hash=sha256:129ed1fb1d23e6fbf8b8e697c2e7340d99bc6271b8c59f9572f3f47063a42e6a \ + --hash=sha256:15a5da8a0a84d75b55fafd56630578131c3c9186e4e486b4d2fb15c349b844d0 \ + --hash=sha256:1db9e95438472f6ed53fa2f4e2811c2d84f4085eeacc3cb8820d770d1ea61d1c \ + --hash=sha256:1f183ab42655ad09b064a4e8eb9cd1eaa138b90ca2f0cd82a200afda062063a5 \ + --hash=sha256:242b8f101301ab303366109d0dfe3cf0db745bf778f7b859fb486105197577d1 \ + --hash=sha256:2b5ff962b0e586953f95b63ec047e1d7a3b6a12a13d11c6e909e0bcd3e05b445 \ + --hash=sha256:2c00922548a666b026e2630a563090341d74c8222066e9c84c9673395bca7363 \ + --hash=sha256:2f53caf9efebcc9040a92c977dcdae78dd0ff4de218fd316e4fcaffd9ace8dc1 \ + --hash=sha256:34fd2bb39cbfa6a8815b5cc99008d59057b9d341db00c67dbb40a3784a8dfb08 \ + --hash=sha256:46cbadf360bbadb6a106217e104b91f85dd618658caffdaab5d54a14d0d52563 \ + --hash=sha256:4fdd54fa57b85d50c4b83ab7ffd90ba2ffcc3d725e3f8d5ffa1ff5f500ef6b97 \ + --hash=sha256:5208b6f10956ca92efb73375364c81a7df365b441b07f4941a24ee0f1bd9e292 \ + --hash=sha256:6e998ee0ffa58342b4d44f2843b036015e3a6326b53185c5361fea4430658466 \ + --hash=sha256:6f28facb5800c0617f233c1db0e622da83de1f74ca28d0ff8646e360d4fda74e \ + --hash=sha256:764992af1ee1cd6d6f26373d09ddb5ede639d025ce9ff658b3b6580dc2ba4ec6 \ + --hash=sha256:87ebbae7ba52f4de9b9f45029a3167d6515399138048d0b734c9033fda7fd723 \ + --hash=sha256:89a975de356d0608e74f1f493442fb3acbbb7a85b739e074460bb0340014b39c \ + --hash=sha256:8bc267738a85f633142c246dceefa722b653e7ba99f02e86dd9a7b980467eafc \ + --hash=sha256:8fbd6d88935749ae892445691ac0dbd1923fc6d8094753a35150fc7756118fe3 \ + --hash=sha256:9b3bf27c51fb46bba993695eebd0c39a4e2a792b707e65b28ac4e8ae703f93d4 \ + --hash=sha256:c33e3d746c3e7a324dbd76b236fe1e44304d5b6d941a1f724f419d01666d6d88 \ + --hash=sha256:c682967736228cc4477e63db0e8854375dd31d755de55b30256de98f1f7b7c23 \ + --hash=sha256:ca9da00bfa95fbf8475d22aba6d7d046f3821a107b733fc7c7c35c74fcfa2bbf \ + --hash=sha256:d1eb40fe68121753f43fc82d618a2eae53dd0731689e124ef9e002aa2c241c4f \ + --hash=sha256:d934aff5fe81e84a45098e281f969976963cc16b3401176a8171affd84301a27 \ + --hash=sha256:e604898ca1790c9fd2e2dc83b38f9185556ea618a3a6e6be31c286fafbebd165 \ + --hash=sha256:eaff9388a2fed0757bd0b4c41c9346d0edea9e7e938a4bfa8070eaabbb538a23 \ + --hash=sha256:f18536d6f97faa81ed6c9af7bb2e27b376b41b27399f862e3b13387538c966b9 + # via astroplan +astropy-iers-data==0.2024.4.29.0.28.48 \ + --hash=sha256:6127bd0b8f9dae569f2400cd37563df05a920d9226c11affc8b8a3413b868fdc \ + --hash=sha256:a2d5acf97e731f1d4a0eab1c8e4c7f454ddc166af06797b141202dd901bd1dfc + # via astropy certifi==2024.2.2 \ --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 @@ -113,9 +150,9 @@ cryptography==42.0.5 \ # via # pyjwt # safir -fastapi==0.110.1 \ - --hash=sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc \ - --hash=sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad +fastapi==0.110.3 \ + --hash=sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626 \ + --hash=sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32 # via safir gidgethub==5.3.0 \ --hash=sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0 \ @@ -179,101 +216,159 @@ idna==3.7 \ # via # anyio # httpx +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f + # via + # astroplan + # astropy + # pyerfa +packaging==24.0 \ + --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ + --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 + # via astropy pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi -pydantic==2.7.0 \ - --hash=sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352 \ - --hash=sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383 +pydantic==2.7.1 \ + --hash=sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5 \ + --hash=sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc # via # fastapi # pydantic-settings # safir -pydantic-core==2.18.1 \ - --hash=sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6 \ - --hash=sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb \ - --hash=sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0 \ - --hash=sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6 \ - --hash=sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47 \ - --hash=sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a \ - --hash=sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a \ - --hash=sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac \ - --hash=sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88 \ - --hash=sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db \ - --hash=sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d \ - --hash=sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d \ - --hash=sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9 \ - --hash=sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e \ - --hash=sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b \ - --hash=sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d \ - --hash=sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649 \ - --hash=sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c \ - --hash=sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1 \ - --hash=sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09 \ - --hash=sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0 \ - --hash=sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90 \ - --hash=sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d \ - --hash=sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294 \ - --hash=sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144 \ - --hash=sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b \ - --hash=sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1 \ - --hash=sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b \ - --hash=sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2 \ - --hash=sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad \ - --hash=sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622 \ - --hash=sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17 \ - --hash=sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06 \ - --hash=sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc \ - --hash=sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50 \ - --hash=sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d \ - --hash=sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59 \ - --hash=sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539 \ - --hash=sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a \ - --hash=sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b \ - --hash=sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5 \ - --hash=sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9 \ - --hash=sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278 \ - --hash=sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6 \ - --hash=sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44 \ - --hash=sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0 \ - --hash=sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb \ - --hash=sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80 \ - --hash=sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5 \ - --hash=sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570 \ - --hash=sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b \ - --hash=sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de \ - --hash=sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6 \ - --hash=sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8 \ - --hash=sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203 \ - --hash=sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7 \ - --hash=sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048 \ - --hash=sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae \ - --hash=sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89 \ - --hash=sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f \ - --hash=sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926 \ - --hash=sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2 \ - --hash=sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76 \ - --hash=sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d \ - --hash=sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411 \ - --hash=sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9 \ - --hash=sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2 \ - --hash=sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586 \ - --hash=sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35 \ - --hash=sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c \ - --hash=sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143 \ - --hash=sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6 \ - --hash=sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60 \ - --hash=sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b \ - --hash=sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226 \ - --hash=sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519 \ - --hash=sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31 \ - --hash=sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7 \ - --hash=sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b +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 # via pydantic pydantic-settings==2.2.1 \ --hash=sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed \ --hash=sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091 +pyerfa==2.0.1.4 \ + --hash=sha256:39cf838c9a21e40d4e3183bead65b3ce6af763c4a727f87d84909c9be7d3a33c \ + --hash=sha256:46d3bed0ac666f08d8364b34a00b8c6595358d6c4f4532da8d13fac0e5227baa \ + --hash=sha256:610d2bc314e140d876b93b1287c7c81685434873c8700cc3e1596193f77d1071 \ + --hash=sha256:7e4508dd7ffd7b27b7f67168643764454887e990ca9e4584824f0e3ab5884c0f \ + --hash=sha256:83a44ba84ebfc3244412ecbf1065c087c382da84f1c3eee1f2a0638d9046ac96 \ + --hash=sha256:88a8d0f3608a66871615bd168fcddf674dce9f7568c239a03cf8d9936161d032 \ + --hash=sha256:900b266a3862baa9560d6b1b184dcc14e0e76d550ff70d32336d3989b2ed18ca \ + --hash=sha256:9045e9f786c76cb55da86ada3405c378c32b88f6e3c6296cb288496ab374b068 \ + --hash=sha256:acb8a6713232ea35c04bc6e40ac4e461dfcc817d395ef2a3c8051c1a33249dd3 \ + --hash=sha256:bc3cf45967ac1af77a777deb050fb08bbc75256dd97ca6005e4d385358b7af40 \ + --hash=sha256:ff112353944bf705342741f2fe41674f97154a302b0295eaef7381af92ad2b3a + # via astropy pyjwt==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 @@ -284,6 +379,13 @@ python-dotenv==1.0.1 \ # via # pydantic-settings # uvicorn +python-slugify==8.0.4 \ + --hash=sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8 \ + --hash=sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856 +pytz==2024.1 \ + --hash=sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812 \ + --hash=sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319 + # via astroplan pyyaml==6.0.1 \ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ @@ -336,10 +438,16 @@ pyyaml==6.0.1 \ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f - # via uvicorn + # via + # astropy + # uvicorn safir==5.2.2 \ --hash=sha256:13069fb1413443be3685e337ce54f25fc427a86ff718c1e3ca44daaaf5c03d34 \ --hash=sha256:e6ad2553e60d1b74bfdbb47bda83776843bb74c3599760b66391253bb0bd4ddb +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via astroplan sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc @@ -356,6 +464,10 @@ structlog==24.1.0 \ --hash=sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d \ --hash=sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16 # via safir +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 + # via python-slugify typing-extensions==4.11.0 \ --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \ --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a diff --git a/requirements/tox.in b/requirements/tox.in new file mode 100644 index 0000000..fde253f --- /dev/null +++ b/requirements/tox.in @@ -0,0 +1,14 @@ +# -*- conf -*- +# +# Editable tox dependencies +# Add tox and its plugins here. These will be installed in the user's venv for +# local development and by CI when running tox actions. +# +# After editing, update requirements/dev.txt by running: +# make update-deps + +-c main.txt +-c dev.txt + +tox +tox-uv diff --git a/requirements/tox.txt b/requirements/tox.txt new file mode 100644 index 0000000..3659f1e --- /dev/null +++ b/requirements/tox.txt @@ -0,0 +1,75 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --output-file requirements/tox.txt requirements/tox.in +cachetools==5.3.3 \ + --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ + --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 + # via tox +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via tox +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via tox +distlib==0.3.8 \ + --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ + --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 + # via virtualenv +filelock==3.14.0 \ + --hash=sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f \ + --hash=sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a + # via + # tox + # virtualenv +packaging==24.0 \ + --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ + --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 + # via + # pyproject-api + # tox + # tox-uv +platformdirs==4.2.1 \ + --hash=sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf \ + --hash=sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1 + # via + # tox + # virtualenv +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via tox +pyproject-api==1.6.1 \ + --hash=sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538 \ + --hash=sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675 + # via tox +tox==4.15.0 \ + --hash=sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea \ + --hash=sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6 + # via tox-uv +tox-uv==1.8.0 \ + --hash=sha256:35347683c52793c2827ba30f6254fbea774bd1beddc9136fb1203ff093bfdddf \ + --hash=sha256:e36149f8721fe11668cad14b78d61d8fdedf6f6d2c56d2e00447b96a475b671b +uv==0.1.39 \ + --hash=sha256:2333dd52e6734e0da6722bdd7b7257d0f8beeac89623c5cfc3888b4c56bc812e \ + --hash=sha256:2ae930189742536f8178617c4ec05cb10271cb3886f6039abd36ee6ab511b160 \ + --hash=sha256:2bda6686a9bb1370d7f53436d34f8ede0fa1b9877b5e152aedd9b22fc3cb33a9 \ + --hash=sha256:3330bd7ab8a6160d815fdc36f48479edf6db8b58d39d20959555095ea7eb63c5 \ + --hash=sha256:3365e0631a738a482d2379e565a230b135f7c5665394313829ccabf7c76c1362 \ + --hash=sha256:388018659e5d73fdeb8ce13c1d812391ec981bf446ab86fb9c0e3d227f727da2 \ + --hash=sha256:4c6ee1148f23aa5d6edf1a1106cc33c4aa57bdbfe8d4c5068c672105415d3b99 \ + --hash=sha256:6b2acc907f7a1735dd9ffeb20d8c7aeeb86b1e5ba0a999e09433ad7f2789dc78 \ + --hash=sha256:7848d703201e6867ae2c70d611e6ffd53d5e5adfc2c9abe89b6d021975e43e81 \ + --hash=sha256:7ee426e0c5fa048cc44f3ac78e476121ef4365bb8bc9199d3cbffc372a80e55d \ + --hash=sha256:88f5601ee957f9be2efc7a24d186f9d2641053806e107e0e42c5e522882c89e0 \ + --hash=sha256:93217578e68a431df235173e390ad7df090499367cd7f5c811520fd4ea3d5047 \ + --hash=sha256:c131dba5fe5079d9c5f06846649e35662901a9afd9b31de17714c63e042d91d2 \ + --hash=sha256:c20b9023dac12ee518de79c91df313be7abb052440cb78f8ffb20dea81d3289e \ + --hash=sha256:cd6d9629ab0e22ab2336b8d6363573ea5a7060ef82ff5d3e6da4b1b30522ef13 \ + --hash=sha256:ce911087f56edc97a5792c17f682ed7611fedead0ea117f56bb6f3942eb3e7b3 \ + --hash=sha256:fba96b3049aea5c1394cd360e5900e4af39829df48ed6fc55eba115c00c8195a + # via tox-uv +virtualenv==20.26.1 \ + --hash=sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b \ + --hash=sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75 + # via tox diff --git a/src/fastapibootcamp/config.py b/src/fastapibootcamp/config.py index 201f67a..dc4acc0 100644 --- a/src/fastapibootcamp/config.py +++ b/src/fastapibootcamp/config.py @@ -26,6 +26,17 @@ class Config(BaseSettings): LogLevel.INFO, title="Log level of the application's logger" ) + clear_iers_on_startup: bool = Field( + False, + title="Clear IERS cache on application startup", + description=( + "The IERS cache is used by Astropy and Astroplan. In development " + "this cache can be cleared on startup to exercise populating it. " + "Generally the cache is not pre-existing in production, so this " + "option is not needed." + ), + ) + model_config = SettingsConfigDict( env_prefix="FASTAPI_BOOTCAMP_", case_sensitive=False ) diff --git a/src/fastapibootcamp/dependencies/__init__.py b/src/fastapibootcamp/dependencies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapibootcamp/dependencies/requestcontext.py b/src/fastapibootcamp/dependencies/requestcontext.py new file mode 100644 index 0000000..7452ed7 --- /dev/null +++ b/src/fastapibootcamp/dependencies/requestcontext.py @@ -0,0 +1,87 @@ +"""Request context FastAPI dependency.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated, Any + +from fastapi import Depends, Request, Response +from httpx import AsyncClient +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 + +# This RequestContext dataclass is what will be provided by the +# context_dependency FastAPI dependency. This is a useful pattern that wraps +# up multiple FastAPI dependencies into one container, and is a useful +# place to put methods that work on those dependencies (e.g., like +# binding context to the logger. +# +# The RequestContext would hold the sqlalchemy Session for apps that use +# databases, or a Redis client, etc. + + +@dataclass +class RequestContext: + """Holds the incoming request and its surrounding context. + + The primary reason for the existence of this class is to allow the + functions involved in request processing to repeatedly rebind the request + logger to include more information, without having to pass both the + request and the logger separately to every function. + """ + + request: Request + """The incoming request.""" + + response: Response + """The response to be returned. + + The response can be modified to include additional headers or status codes. + """ + + logger: BoundLogger + """The request logger, rebound with discovered context.""" + + http_client: AsyncClient + """The HTTPX async HTTP client.""" + + factory: Factory + """The component factory.""" + + def rebind_logger(self, **values: Any) -> None: + """Add the given values to the logging context. + + Also updates the logging context stored in the request object in case + the request context later needs to be recreated from the request. + + Parameters + ---------- + **values + Additional values that should be added to the logging context. + """ + self.logger = self.logger.bind(**values) + + +# This is the FastAPI dependency that provides the RequestContext. Notice +# how dependency function signatures look like FastAPI path operations. +# This context dependency takes the request and response arguemnts available +# to path operations, and also depends on other dependencies. + + +async def context_dependency( + request: Request, + response: Response, + logger: Annotated[BoundLogger, Depends(logger_dependency)], + http_client: Annotated[AsyncClient, Depends(http_client_dependency)], +) -> RequestContext: + """Provide a RequestContext as a dependency.""" + return RequestContext( + request=request, + response=response, + logger=logger, + http_client=http_client, + factory=Factory(logger=logger, http_client=http_client), + ) diff --git a/src/fastapibootcamp/domain/__init__.py b/src/fastapibootcamp/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapibootcamp/domain/models.py b/src/fastapibootcamp/domain/models.py new file mode 100644 index 0000000..8554ca6 --- /dev/null +++ b/src/fastapibootcamp/domain/models.py @@ -0,0 +1,96 @@ +"""Models for the Astroplan-related app domain.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from astroplan import Observer as AstroplanObserver +from astropy.coordinates import AltAz, Angle, SkyCoord +from astropy.time import Time + +__all__ = ["Observer", "TargetObservability"] + +# The domain layer is where your application's core business logic resides. +# In this demo, the domain is built around the Astroplan library and its +# Observer class. In fact, we subclass Astroplan's Observer to add some +# additional attributes that are useful for our application. +# +# Note that the domain layer doesn't have dependendencies on other layers like +# the storage or web API layers (unlike the service layer, which knows about +# the storage layer in abstract detail). The domain doesn't care that its +# in the middle of a FastAPI app. Every other layer of the app "cares" about +# the domain, though. + + +class Observer(AstroplanObserver): + """The observer domain model.""" + + def __init__( + self, + observer_id: str, + local_timezone: str, + aliases: list[str] | None = None, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + + self.observer_id = observer_id + self.aliases = list(aliases) if aliases else [] + self.local_timezone = local_timezone + + +@dataclass(kw_only=True) +class TargetObservability: + """The observability of a target for an observer.""" + + observer: Observer + target: SkyCoord + time: datetime + airmass: float | None + altaz: AltAz + is_above_horizon: bool + is_night: bool + moon_up: bool + moon_separation: Angle + moon_altaz: AltAz + moon_illumination: float + + @classmethod + def compute( + cls, + observer: Observer, + target: SkyCoord, + time: datetime, + ) -> TargetObservability: + """Compute the observability of a target for an observer.""" + astropy_time = Time(time) + is_up = observer.target_is_up(astropy_time, target) + + altaz = target.transform_to( + AltAz(obstime=astropy_time, location=observer.location) + ) + airmass = altaz.secz if is_up else None + + is_night = observer.is_night(astropy_time) + + moon_altaz = observer.moon_altaz(astropy_time) + moon_up = moon_altaz.alt > 0 + moon_separation = altaz.separation(moon_altaz) + moon_illumination = observer.moon_illumination(astropy_time) + + return cls( + observer=observer, + target=target, + time=time, + is_above_horizon=is_up, + airmass=airmass, + altaz=altaz, + is_night=is_night, + moon_up=moon_up, + moon_separation=moon_separation, + moon_altaz=moon_altaz, + moon_illumination=moon_illumination, + ) diff --git a/src/fastapibootcamp/exceptions.py b/src/fastapibootcamp/exceptions.py new file mode 100644 index 0000000..8b2ed56 --- /dev/null +++ b/src/fastapibootcamp/exceptions.py @@ -0,0 +1,15 @@ +"""Exceptions for the FastAPI Bootcamp app.""" + +from safir.fastapi import ClientRequestError + +__all__ = [ + "ObserverNotFoundError", +] + + +class ObserverNotFoundError(ClientRequestError): + """Raised when an observing site is not found.""" + + status_code = 404 + + error = "unknown_observer" diff --git a/src/fastapibootcamp/factory.py b/src/fastapibootcamp/factory.py new file mode 100644 index 0000000..ea4dd84 --- /dev/null +++ b/src/fastapibootcamp/factory.py @@ -0,0 +1,32 @@ +"""Component factory for the application.""" + +from httpx import AsyncClient +from structlog.stdlib import BoundLogger + +from .services.observerservice import ObserverService +from .storage.observerstore import ObserverStore + +# Your FastAPI app will have a lot of components: services, and stores. The +# Factory is a convenient pattern for creating these components. For example +# a service might need a store, so the factory can encapsululate the +# pattern of creating both. + + +class Factory: + """A factory for the application components.""" + + def __init__( + self, *, logger: BoundLogger, http_client: AsyncClient + ) -> None: + self.logger = logger + self.http_client = http_client + + def create_observer_service(self) -> ObserverService: + """Create an observer service.""" + return ObserverService( + logger=self.logger, observer_store=self.create_observer_store() + ) + + def create_observer_store(self) -> ObserverStore: + """Create an observer store.""" + return ObserverStore(logger=self.logger) diff --git a/src/fastapibootcamp/handlers/astroplan/__init__.py b/src/fastapibootcamp/handlers/astroplan/__init__.py new file mode 100644 index 0000000..9823e05 --- /dev/null +++ b/src/fastapibootcamp/handlers/astroplan/__init__.py @@ -0,0 +1,3 @@ +from .endpoints import astroplan_router + +__all__ = ["astroplan_router"] diff --git a/src/fastapibootcamp/handlers/astroplan/endpoints.py b/src/fastapibootcamp/handlers/astroplan/endpoints.py new file mode 100644 index 0000000..2a9b565 --- /dev/null +++ b/src/fastapibootcamp/handlers/astroplan/endpoints.py @@ -0,0 +1,146 @@ +"""Path operations for the astroplan router.""" + +from typing import Annotated, TypeAlias + +from fastapi import APIRouter, Depends, Path, Query +from safir.models import ErrorLocation + +from fastapibootcamp.dependencies.requestcontext import ( + RequestContext, + context_dependency, +) +from fastapibootcamp.exceptions import ObserverNotFoundError + +from .models import ( + ObservabilityResponseModel, + ObservationRequestModel, + ObserverModel, +) + +astroplan_router = APIRouter(tags=["astroplan"]) + +# The core of a RESTful API is the "resource". In this API, observing sites are +# the resource. This first endpoint lets the user retrieve an existing +# observing site resource by its ID. +# +# e.g. GET /astroplan/observers/rubin + +# We can declare path parameters that are used commonly across multiple +# endpoints in a single place. Since this is a type annotation, we can use +# the type keyword in Python 3.12, which is equivalent to the TypeAlias type +# before. + +ObserverIdPathParam: TypeAlias = Annotated[ + str, + Path( + ..., + title="The observer's site ID.", + examples=["rubin", "rubin-aux", "gemini-north"], + ), +] + + +@astroplan_router.get( + "/observers/{observer_id}", + summary="Get an observer by site ID.", + response_model=ObserverModel, +) +async def get_observer( + observer_id: ObserverIdPathParam, + context: Annotated[RequestContext, Depends(context_dependency)], +) -> ObserverModel: + # Use the request context and factory patterns to get an observer service. + factory = context.factory + observer_service = factory.create_observer_service() + + try: + # Run a method on the observer service to get the observer + observer = await observer_service.get_observer_by_id(observer_id) + except ObserverNotFoundError as e: + # The service, and components it calls, can raise exceptions because + # the user input is invalid. To provide a meaningful error response, + # the handler augments the exception with information about what + # user input caused the error. + # + # The model for this error message is declared on the router in + # main.py. + e.location = ErrorLocation.path + e.field_path = ["observer_id"] + raise + + # Transform the domain object into a response object. + return ObserverModel.from_domain( + observer=observer, request=context.request + ) + + +# RESTFul APIs commonly let clients list all resources of a type, usually +# in conjunction with filtering. This endpoint lets the user list all +# observing sites, optionally filtering by name or alias. +# +# e.g. GET /astroplan/observers?name=rubin +# +# Note: in a production you'll usually implement "pagination" to limit the +# response size. This isn't done here, but could be a good exercise for +# an advanced bootcamp class. + + +@astroplan_router.get( + "/observers", + summary="Get all observering sites.", + response_model=list[ObserverModel], +) +async def get_observers( + context: Annotated[RequestContext, Depends(context_dependency)], + name_pattern: Annotated[ + str | None, + Query( + alias="name", + description="Filter by observing site name or alias.", + examples=["rubin", "lsst", "gemini"], + ), + ] = None, +) -> list[ObserverModel]: + factory = context.factory + observer_service = factory.create_observer_service() + + observers = await observer_service.get_observers(name_pattern=name_pattern) + + return [ + ObserverModel.from_domain(observer=observer, request=context.request) + for observer in observers + ] + + +# This is a POST endpoint. A POST request lets the client send a JSON payload. +# Often this is used to create a new resource, but in this case we're using it +# to trigger a calculation that's too complex to be done in a GET endpoint +# with a query string. +# +# e.g. POST /astroplan/observers/rubin/observable + + +@astroplan_router.post( + "/observers/{observer_id}/observable", + summary=( + "Check if a coordinate is observable for an observer at a given time." + ), + response_model=ObservabilityResponseModel, +) +async def post_observable( + observer_id: ObserverIdPathParam, + request_data: ObservationRequestModel, + context: Annotated[RequestContext, Depends(context_dependency)], +) -> ObservabilityResponseModel: + factory = context.factory + observer_service = factory.create_observer_service() + + observability = await observer_service.get_target_observability( + observer_id=observer_id, + sky_coord=request_data.get_target(), + time=request_data.time, + ) + + return ObservabilityResponseModel.from_domain( + observability=observability, request=context.request + ) diff --git a/src/fastapibootcamp/handlers/astroplan/models.py b/src/fastapibootcamp/handlers/astroplan/models.py new file mode 100644 index 0000000..e6eb242 --- /dev/null +++ b/src/fastapibootcamp/handlers/astroplan/models.py @@ -0,0 +1,222 @@ +"""Web API models for the astroplan router.""" + +# In a FastAPI application, you'll have lots of Pydantic models or dataclasses. +# These are your "models." It's wise to maintain separate models for different +# areas of your application. In this module we're defining the Pydantic models +# that define the request and response JSON payloads for the web API. Notice +# how the response models have class methods that convert domain (internal) +# models to the web API model. Using this pattern lets you change how data is +# represented internally or in databases without inadverently changing the +# public web API. + +from __future__ import annotations + +from datetime import datetime +from typing import Self + +from astropy.coordinates import SkyCoord +from fastapi import Request +from pydantic import BaseModel, Field, field_validator, model_validator +from safir.datetime import current_datetime +from safir.pydantic import normalize_isodatetime + +from fastapibootcamp.domain.models import Observer, TargetObservability + +__all__ = [ + "ObserverModel", + "ObservationRequestModel", + "ObservabilityResponseModel", +] + + +class ObserverModel(BaseModel): + """Model for an observer resource response.""" + + id: str = Field( + ..., + description="URL-safe identifier of the site.", + examples=["rubin", "rubin-auxtel"], + ) + + name: str = Field( + ..., + description="Short name of the observation site.", + examples=["Rubin Observatory"], + ) + + aliases: list[str] = Field( + description="Aliases for the observation site.", + default_factory=list, + examples=[["LSST", "LSST 8.4m", "Rubin"]], + ) + + local_timezone: str = Field( + ..., + description="Local timezone of the observation site.", + examples=["Chile/Continental"], + ) + + longitude: float = Field( + description="Longitude of the observation site (degrees).", + examples=[-70.74772222222225], + ) + + latitude: float = Field( + description="Latitude of the observation site (degrees).", + examples=[-30.244633333333333], + ) + + elevation: float = Field( + description="Elevation of the observation site (meters).", + examples=[2662.75], + ) + + self_url: str = Field(description="URL to the observer resource.") + + # This class method consructs the API model for the observer resource + # from the internal domain, which is astropy's Observer object here. + # Passing the FastAPI request lets us construct URLs to related resources. + + @classmethod + def from_domain( + cls, *, observer: Observer, request: Request + ) -> ObserverModel: + """Create a model from an astroplain Observer domain object.""" + return cls( + id=observer.observer_id, + name=observer.name, + aliases=observer.aliases, + local_timezone=observer.local_timezone, + longitude=observer.longitude.deg, + latitude=observer.latitude.deg, + elevation=observer.elevation.to_value("m"), + # Including URLs to related responses, and even the response + # itself, is a good practice. This allows clients to make requests + # to without having to construct URLs themselves. + self_url=str( + request.url_for( + "get_observer", observer_id=observer.observer_id + ) + ), + ) + + +class ObservationRequestModel(BaseModel): + """Model for an observation request.""" + + ra: str = Field( + ..., + description="Target right ascension (HHhMMmSSs).", + examples=["5h23m34.6s"], + ) + + dec: str = Field( + ..., + description="Target declination (DDdMMmSSm).", + examples=["-69d45m22s"], + ) + + time: datetime = Field( + description="Time of the observation. Defaults to now if unset.", + default_factory=current_datetime, + examples=["2024-04-24T00:00:00Z"], + ) + + # This ensures that the time is always provided in UTC. + + _normalize_time = field_validator("time", mode="before")( + normalize_isodatetime + ) + + @model_validator(mode="after") + def validate_coordinate(self) -> Self: + # Validate ra and dec by try to parse them into an astropy SkyCoord + # object. If they're invalid, this will raise a ValueError. + try: + self.get_target() + except Exception as e: + raise ValueError( + f"Invalid coordinates: ra={self.ra}, dec={self.dec}" + ) from e + return self + + def get_target(self) -> SkyCoord: + """Get the target RA and Dec as an astropy SkyCoord.""" + return SkyCoord( + self.ra, + self.dec, + frame="icrs", + ) + + +class ObservabilityResponseModel(BaseModel): + """Model for an observability response.""" + + ra: str = Field( + ..., + description="Target right ascension (HHhMMmSSs).", + examples=["5h23m34.6s"], + ) + + dec: str = Field( + ..., + description="Target declination (DDdMMmSSm).", + examples=["-69d45m22s"], + ) + + time: datetime = Field( + ..., + description="Time of the observation (UTC).", + examples=["2024-04-24T00:00:00Z"], + ) + + is_night: bool = Field( + ..., + description="Whether it is night time at the time of the observation.", + ) + + above_horizon: bool = Field( + ..., description="Whether the target is above the horizon." + ) + + airmass: float | None = Field( + None, description="Airmass of the target (null if below horizon)." + ) + + alt: float = Field(..., description="Altitude of the target (degrees).") + + az: float = Field(..., description="Azimuth of the target (degrees).") + + moon_separation: float = Field( + ..., description="Separation from the moon (degrees)." + ) + + moon_illumination: float = Field( + ..., description="Illumination of the moon (fraction)." + ) + + observer_url: str = Field(..., description="URL to the observer resource.") + + @classmethod + def from_domain( + cls, *, observability: TargetObservability, request: Request + ) -> ObservabilityResponseModel: + """Create a model from a TargetObservability domain object.""" + return cls( + ra=observability.target.ra.to_string(unit="hourangle"), + dec=observability.target.dec.to_string(unit="degree"), + time=observability.time, + above_horizon=observability.is_above_horizon, + is_night=observability.is_night, + airmass=observability.airmass, + alt=observability.altaz.alt.deg, + az=observability.altaz.az.deg, + moon_separation=observability.moon_separation.deg, + moon_illumination=observability.moon_illumination, + observer_url=str( + request.url_for( + "get_observer", + observer_id=observability.observer.observer_id, + ) + ), + ) diff --git a/src/fastapibootcamp/main.py b/src/fastapibootcamp/main.py index 83dc4b6..0cc80d5 100644 --- a/src/fastapibootcamp/main.py +++ b/src/fastapibootcamp/main.py @@ -13,26 +13,48 @@ from fastapi import FastAPI from safir.dependencies.http_client import http_client_dependency +from safir.fastapi import ClientRequestError, client_request_error_handler from safir.logging import configure_logging, configure_uvicorn_logging from safir.middleware.x_forwarded import XForwardedMiddleware +from safir.models import ErrorModel +from structlog import get_logger +# Notice how the the config instance is imported early so it's both +# instantiated on app start-up and available to set up the app. from .config import config +from .handlers.astroplan import astroplan_router from .handlers.external import external_router from .handlers.internal import internal_router +from .storage.iers import IersCacheManager __all__ = ["app", "config"] +# The lifespan context manager is used to set up and tear down anything that +# lives for the duration of the application. This is where you would put +# database connections, etc. + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: """Set up and tear down the application.""" # Any code here will be run when the application starts up. + logger = get_logger(__name__) + iers_cache_manager = IersCacheManager(logger) + iers_cache_manager.config_iers_cache() + if config.clear_iers_on_startup: + iers_cache_manager.clear_iers_cache() + iers_cache_manager.download_iers_data() + logger.info("fastapi-bootcamp application startup complete.") yield # Any code here will be run when the application shuts down. await http_client_dependency.aclose() + logger.info("fastapi-bootcamp application shut down complete.") + +# The Safir library helps you set up logging around structlog, +# https://www.structlog.org/en/stable/ configure_logging( profile=config.profile, @@ -41,6 +63,10 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: ) configure_uvicorn_logging(config.log_level) +# The FastAPI application is created here. In our Docker image, we run the +# application by running the Uvicorn server that points to the `app` instance +# in this module. + app = FastAPI( title="fastapi-bootcamp", description=metadata("fastapi-bootcamp")["Summary"], @@ -52,9 +78,32 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: ) """The main FastAPI application for fastapi-bootcamp.""" -# Attach the routers. +# Attach the routers to the FastAPI application. +# Each router is associated with path operation functions (HTTP endpoints). +# Generally different routers are grouped around different URL path prefixes. +# The internal router doesn't have a URL path prefix. It isn't exposed to the +# internet since the Kubernetes Ingress typically routes to a path. We use +# this for health checks and internal monitoring. app.include_router(internal_router) +# The external router is the main router for the service. It has a URL path +# prefix that is the same as the Kubernetes Ingress path. Generally your REST +# API will be built around this external router. app.include_router(external_router, prefix=f"{config.path_prefix}") +# We're building a more "realistic" demonstration API around the Astroplan +# package. To help keep the code organized, we've created a separate router, +# but you generally won't do this for your own apps. +app.include_router( + astroplan_router, + prefix=f"{config.path_prefix}/astroplan", + # Common errors are modelled around Safir's ErrorModel class, so we can + # set that here for all endpoints on this router. + responses={ + 404: {"description": "Not found", "model": ErrorModel}, + }, +) # Add middleware. app.add_middleware(XForwardedMiddleware) + +# Add exception handler for Safir's ClientRequestError. +app.exception_handler(ClientRequestError)(client_request_error_handler) diff --git a/src/fastapibootcamp/services/__init__.py b/src/fastapibootcamp/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapibootcamp/services/observerservice.py b/src/fastapibootcamp/services/observerservice.py new file mode 100644 index 0000000..8a6467b --- /dev/null +++ b/src/fastapibootcamp/services/observerservice.py @@ -0,0 +1,83 @@ +"""The observer service.""" + +from __future__ import annotations + +from datetime import datetime + +from astropy.coordinates import SkyCoord +from structlog.stdlib import BoundLogger + +from ..domain.models import Observer, TargetObservability +from ..exceptions import ObserverNotFoundError +from ..storage.observerstore import ObserverStore + +# The service layer is what orchestrates the application's "business logic". +# By orchestration, we mean that the service layer will get data from the +# storage layer, call the domain to do the actual work, and then store that +# result and/or return a domain object to the caller. +# +# The service layer helps separate your application's domain from the web API. +# In fact, the service layer could also be called by a command-line interface, +# be run through an async task queue like arq or celery, or of course be called +# in tests. + + +class ObserverService: + """A service to orchestrating the Astroplan observer domain.""" + + def __init__( + self, *, observer_store: ObserverStore, logger: BoundLogger + ) -> None: + self._logger = logger + self._observer_store = observer_store + + async def get_observer_by_id(self, observer_id: str) -> Observer: + """Get an observer by site ID.""" + observer = await self._observer_store.get_observer_by_id(observer_id) + if observer is None: + raise ObserverNotFoundError(observer_id) + return observer + + async def get_observers( + self, name_pattern: str | None = None + ) -> list[Observer]: + """Get all observers, possibly filtering on attributes. + + Parameters + ---------- + name_pattern + A pattern to match against observer names. + + Returns + ------- + list of Observer + Observers that match any query patterns, or all observers if no + query is provided. + """ + return await self._observer_store.get_observers( + name_pattern=name_pattern + ) + + async def get_target_observability( + self, observer_id: str, sky_coord: SkyCoord, time: datetime + ) -> TargetObservability: + """Get the observability of a target for an observer. + + Parameters + ---------- + observer_id + The ID of the observer. + sky_coord + The target's coordinates. + time + The time of observation. + + Returns + ------- + TargetObservability + The observability domain model based on observer, target, and time. + """ + observer = await self.get_observer_by_id(observer_id) + return TargetObservability.compute( + observer=observer, target=sky_coord, time=time + ) diff --git a/src/fastapibootcamp/storage/README.md b/src/fastapibootcamp/storage/README.md new file mode 100644 index 0000000..e346a50 --- /dev/null +++ b/src/fastapibootcamp/storage/README.md @@ -0,0 +1 @@ +`sites.json` is courtesy of Astropy. It is available from http://www.astropy.org/astropy-data/coordinates/sites.json diff --git a/src/fastapibootcamp/storage/__init__.py b/src/fastapibootcamp/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapibootcamp/storage/iers.py b/src/fastapibootcamp/storage/iers.py new file mode 100644 index 0000000..4c6d33a --- /dev/null +++ b/src/fastapibootcamp/storage/iers.py @@ -0,0 +1,52 @@ +"""Manage IERS the caching for Astropy.""" + +from __future__ import annotations + +from astropy.utils import iers +from astropy.utils.data import clear_download_cache +from structlog.stdlib import BoundLogger + + +class IersCacheManager: + """Manage IERS the caching for Astropy. + + The Astropy and Astroplan calculations of target observability, as well + as the location of the Sun and Moon, use IERS data. To provide good + predictions, we configure IERS to cache the IERS tables, but to update + them as necessary. This class also allows us to prime the cache with + current IERS data. Requests that trigger an IERS download will take longer + and will block the app since Astropy downloads IERS data synchronously. + + Another approach would be to rely on data from `astropy-iers-data` + exclusively, and regularly release and deploy new versions of this app + to include that data. + + Yet another approach might be to use a separate service to maintain the + IERS cache e.g. in Redis and provide it to the app as a service. + """ + + def __init__(self, logger: BoundLogger) -> None: + self._logger = logger + + def config_iers_cache(self) -> None: + """Configure the IERS cache.""" + iers.conf.auto_download = True + iers.conf.auto_max_age = 30 # days + + def download_iers_data(self) -> None: + """Download the IERS data. + + This method downloads and caches IERS data that gives predictions + valid up to a year in the future. + + Note that requesting observability predictions for dates outside this + range may trigger a download of new IERS data. + """ + self._logger.info("Downloading IERS data.") + iers.IERS_Auto.open() + self._logger.info("IERS data download complete.") + + def clear_iers_cache(self) -> None: + """Clear the IERS cache.""" + clear_download_cache() + self._logger.info("IERS cache cleared.") diff --git a/src/fastapibootcamp/storage/observerstore.py b/src/fastapibootcamp/storage/observerstore.py new file mode 100644 index 0000000..78c6e3e --- /dev/null +++ b/src/fastapibootcamp/storage/observerstore.py @@ -0,0 +1,125 @@ +"""The storage for observer objects.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import astropy.units as u +from astropy.coordinates import EarthLocation +from slugify import slugify +from structlog.stdlib import BoundLogger + +from ..domain.models import Observer + +# The storage layer is where your application gets and stores data in external +# systems. Often the store will be a database (SQLAlchemy, Redis, etc.). +# It can also be other web APIs. In a store, you'll have methods for getting +# and storing the domain models. The store might adapt your application's +# internal domain models into the format required by the external system, +# like a SQLAlchemy ORM model or a JSON object for a web API. +# +# For this bootcamp application, we don't want to have external depednencies +# so we'll pretend that astroplan has a database, and use the store to get +# "Observer" domain objects. + + +class ObserverStore: + """A store for Astroplan observer objects. + + Parameters + ---------- + logger + The structlog logger. + """ + + def __init__(self, logger: BoundLogger) -> None: + self._logger = logger + + # For this demo, we're loading the data from a JSON file embedded in + # the app to avoid the complexity of a database. Sometimes stores + # actually are text files from configuration; in that case you'd + # set the path to that data in the app's configuration and have a + # classmethod constructor for the store that loads the data from + # that location. + site_data = json.loads( + Path(__file__).parent.joinpath("sites.json").read_text() + ) + self._sites = {slugify(key): value for key, value in site_data.items()} + + async def get_observer_by_id(self, observer_id: str) -> Observer | None: + """Get an observer from the store. + + Parameters + ---------- + observer_id + The ID of the observer to get. This is the URL-safe slug. + + Returns + ------- + Observer or None + The observer object. If the observer is not found, returns None. + """ + site_info = self._sites.get(observer_id) + if site_info is None: + return None + + location = EarthLocation.from_geodetic( + site_info["longitude"] * u.Unit(site_info["longitude_unit"]), + site_info["latitude"] * u.Unit(site_info["latitude_unit"]), + site_info["elevation"] * u.Unit(site_info["elevation_unit"]), + ) + name = site_info["name"] + return Observer( + observer_id=observer_id, + location=location, + name=name, + aliases=site_info["aliases"], + local_timezone=site_info["timezone"], + ) + + async def get_observers( + self, name_pattern: str | None = None + ) -> list[Observer]: + """Get all observers in the store optionally filtering by attributes. + + Patterns + -------- + name_pattern + A substring to filter observers by name or alias. If None, + all observers are returned. + """ + # Get all observers and ensure we don't have any missing values + # for type checking + all_ids = self._get_observer_ids() + all_observers = [ + await self.get_observer_by_id(observer_id) + for observer_id in all_ids + ] + observers = [ + observer for observer in all_observers if observer is not None + ] + + # Apply filtering by attributes + if name_pattern: + observers = [ + observer + for observer in observers + if self._has_name_pattern(observer, name_pattern) + ] + + return observers + + def _has_name_pattern(self, observer: Observer, name_pattern: str) -> bool: + """Check if the observer matches the name pattern.""" + # We're just doing case-insensitive substring matching here. + pattern = name_pattern.lower() + return pattern in observer.name.lower() or any( + pattern in alias.lower() for alias in observer.aliases + ) + + def _get_observer_ids(self) -> list[str]: + """Get the sorted list of ID of all observers in the Astropy site + store. + """ + return sorted(self._sites.keys()) diff --git a/src/fastapibootcamp/storage/sites.json b/src/fastapibootcamp/storage/sites.json new file mode 100644 index 0000000..b001e30 --- /dev/null +++ b/src/fastapibootcamp/storage/sites.json @@ -0,0 +1,1214 @@ +{ + "greenwich": { + "source": "Ordnance Survey via http://gpsinformation.net/main/greenwich.htm and UNESCO", + "elevation": 46, + "name": "Royal Observatory Greenwich", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 51.477811, + "elevation_unit": "meter", + "longitude": -0.001475, + "timezone": "Greenwich", + "aliases": ["example_site"] + }, + "hcro": { + "source": "SRI website https://archive.sri.com/research-development/specialized-facilities/hat-creek-radio-observatory", + "name": "Hat Creek Radio Observatory", + "longitude": -121.47333, + "latitude": 40.8175, + "elevation": 986, + "longitude_unit": "degree", + "latitude_unit": "degree", + "elevation_unit": "meter", + "timezone": "US/Pacific", + "aliases": ["ATA", "Allen Telescope Array", "Hat Creek"] + }, + "mwo": { + "source": "CHARA website", + "elevation": 1742, + "name": "Mount Wilson Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 34.222579805555554, + "elevation_unit": "meter", + "longitude": -118.06168400000001, + "timezone": "America/Los_Angeles", + "aliases": ["CHARA"] + }, + "tona": { + "source": "IRAF Observatory Database", + "elevation": 0, + "name": "Observatorio Astronomico Nacional, Tonantzintla", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.032777777777778, + "elevation_unit": "meter", + "longitude": 261.68611111111113, + "timezone": "US/Central", + "aliases": ["Observatorio Astronomico Nacional, Tonantzintla"] + }, + "lick": { + "source": "IRAF Observatory Database", + "elevation": 1290, + "name": "Lick Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 37.343333333333334, + "elevation_unit": "meter", + "longitude": 238.36333333333332, + "timezone": "US/Pacific", + "aliases": ["Lick Observatory"] + }, + "mdm": { + "source": "IRAF Observatory Database", + "elevation": 1938, + "name": "MDM Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 31.95, + "elevation_unit": "meter", + "longitude": 248.38333333333333, + "timezone": "US/Mountain", + "aliases": ["Michigan-Dartmouth-MIT Observatory"] + }, + "lapalma": { + "source": "IRAF Observatory Database", + "elevation": 2327, + "name": "Roque de los Muchachos, La Palma", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 28.758333333333333, + "elevation_unit": "meter", + "longitude": 342.12, + "timezone": "Atlantic/Canary", + "aliases": ["Roque de los Muchachos"] + }, + "ctio": { + "source": "IRAF Observatory Database", + "elevation": 2215, + "name": "Cerro Tololo Interamerican Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -30.165277777777778, + "elevation_unit": "meter", + "longitude": 289.185, + "timezone": "Chile/Continental", + "aliases": ["Cerro Tololo Interamerican Observatory", "Cerro Tololo"] + }, + "apo": { + "source": "IRAF Observatory Database", + "elevation": 2798, + "name": "Apache Point Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 32.78, + "elevation_unit": "meter", + "longitude": 254.17999999999998, + "timezone": "US/Mountain", + "aliases": ["Apache Point Observatory", "Apache Point"] + }, + "BAO": { + "source": "IRAF Observatory Database", + "elevation": 950, + "name": "Beijing XingLong Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 40.39333333333333, + "elevation_unit": "meter", + "longitude": 117.575, + "timezone": "Asia/Shanghai", + "aliases": ["Beijing XingLong Observatory"] + }, + "mcdonald": { + "source": "IRAF Observatory Database", + "elevation": 2075, + "name": "McDonald Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 30.671666694444447, + "elevation_unit": "meter", + "longitude": 255.97833330555557, + "timezone": "US/Central", + "aliases": ["McDonald Observatory"] + }, + "mtbigelow": { + "source": "IRAF Observatory Database", + "elevation": 2510, + "name": "Catalina Observatory: 61 inch telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 32.416666666666664, + "elevation_unit": "meter", + "longitude": 249.26833333333335, + "timezone": "US/Mountain", + "aliases": ["Catalina Observatory"] + }, + "mso": { + "source": "IRAF Observatory Database", + "elevation": 767, + "name": "Mt. Stromlo Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -35.32065, + "elevation_unit": "meter", + "longitude": 149.02433333333335, + "timezone": "Australia/Sydney", + "aliases": ["Mt. Stromlo Observatory"] + }, + "lasilla": { + "source": "IRAF Observatory Database", + "elevation": 2347, + "name": "La Silla Observatory (ESO)", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -29.256666666666668, + "elevation_unit": "meter", + "longitude": 289.27, + "timezone": "Chile/Continental", + "aliases": ["La Silla Observatory"] + }, + "paranal": { + "source": "Official reference in VLTI raw data products", + "elevation": 2669, + "name": "Paranal Observatory (ESO)", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -24.62743941, + "elevation_unit": "meter", + "longitude": -70.40498688, + "timezone": "Chile/Continental", + "aliases": ["Cerro Paranal", "Paranal Observatory"] + }, + "cfht": { + "source": "IRAF Observatory Database", + "elevation": 4215, + "name": "Canada-France-Hawaii Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.826666666666668, + "elevation_unit": "meter", + "longitude": 204.52833333333334, + "timezone": "US/Hawaii", + "aliases": ["Canada-France-Hawaii Telescope"] + }, + "lowell": { + "source": "IRAF Observatory Database", + "elevation": 2198, + "name": "Lowell Observatory - Anderson Mesa", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 35.09666666666667, + "elevation_unit": "meter", + "longitude": 248.46499999999997, + "timezone": "US/Mountain", + "aliases": [ + "Lowell Observatory", + "Anderson Mesa", + "lo-am", + "NPOI", + "Navy Precision Optical Interferometer", + "Perkins", + "PTO", + "pto" + ] + }, + "keck": { + "source": "IRAF Observatory Database", + "elevation": 4160, + "name": "W. M. Keck Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.828333333333333, + "elevation_unit": "meter", + "longitude": 204.52166666666668, + "timezone": "US/Hawaii", + "aliases": ["W. M. Keck Observatory", "Keck Observatory"] + }, + "Palomar": { + "source": "IRAF Observatory Database", + "elevation": 1706, + "name": "The Hale Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 33.356, + "elevation_unit": "meter", + "longitude": 243.137, + "timezone": "US/Pacific", + "aliases": ["Hale Telescope"] + }, + "mmt": { + "source": "IRAF Observatory Database", + "elevation": 2608, + "name": "Multiple Mirror Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 31.688333333333333, + "elevation_unit": "meter", + "longitude": 249.11499999999998, + "timezone": "US/Mountain", + "aliases": ["Multiple Mirror Telescope"] + }, + "dao": { + "source": "IRAF Observatory Database", + "elevation": 229, + "name": "Dominion Astrophysical Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 48.52166666666667, + "elevation_unit": "meter", + "longitude": 236.58333333333334, + "timezone": "US/Pacific", + "aliases": ["Dominion Astrophysical Observatory"] + }, + "bmo": { + "source": "IRAF Observatory Database", + "elevation": 738, + "name": "Black Moshannon Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 40.92166666666667, + "elevation_unit": "meter", + "longitude": 281.995, + "timezone": "US/Eastern", + "aliases": ["Black Moshannon Observatory"] + }, + "sso": { + "source": "IRAF Observatory Database", + "elevation": 1149, + "name": "Siding Spring Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -31.27336111111111, + "elevation_unit": "meter", + "longitude": 149.06119444444445, + "timezone": "Australia/Sydney", + "aliases": ["Siding Spring Observatory"] + }, + "lco": { + "source": "IRAF Observatory Database", + "elevation": 2282, + "name": "Las Campanas Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -29.003333333333334, + "elevation_unit": "meter", + "longitude": 289.29833333333335, + "timezone": "Chile/Continental", + "aliases": ["Las Campanas Observatory"] + }, + "kpno": { + "source": "IRAF Observatory Database", + "elevation": 2120, + "name": "Kitt Peak National Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 31.96333333333333, + "elevation_unit": "meter", + "longitude": 248.4, + "timezone": "US/Arizona", + "aliases": ["Kitt Peak", "Kitt Peak National Observatory"] + }, + "aao": { + "source": "IRAF Observatory Database", + "elevation": 1164, + "name": "Anglo-Australian Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -31.27703888888889, + "elevation_unit": "meter", + "longitude": 149.06608611111113, + "timezone": "Australia/Sydney", + "aliases": ["Anglo-Australian Observatory"] + }, + "flwo": { + "source": "IRAF Observatory Database", + "elevation": 2320, + "name": "Whipple Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 31.680944444444446, + "elevation_unit": "meter", + "longitude": 249.1225, + "timezone": "US/Mountain", + "aliases": ["Whipple", "Whipple Observatory"] + }, + "ekar": { + "source": "IRAF Observatory Database", + "elevation": 1413, + "name": "Cima Ekar 182 cm Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 45.84858888888889, + "elevation_unit": "meter", + "longitude": 11.581133333333334, + "timezone": "Europe/Rome", + "aliases": ["Cima Ekar Observing Station", "Mt. Ekar 182 cm Telescope"] + }, + "Subaru": { + "source": "Subaru Telescope website (August 2015)", + "elevation": 4139, + "name": "Subaru Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.825555555555557, + "elevation_unit": "meter", + "longitude": 204.5238888888889, + "timezone": "US/Hawaii", + "aliases": ["Subaru Telescope"] + }, + "vbo": { + "source": "IRAF Observatory Database", + "elevation": 725, + "name": "Vainu Bappu Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 12.57666, + "elevation_unit": "meter", + "longitude": 78.8266, + "timezone": "Asia/Kolkata", + "aliases": ["Vainu Bappu Observatory"] + }, + "NOV": { + "source": "IRAF Observatory Database", + "elevation": 3610, + "name": "National Observatory of Venezuela", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 8.79, + "elevation_unit": "meter", + "longitude": 289.1333333333333, + "timezone": "America/Caracas", + "aliases": ["National Observatory of Venezuela"] + }, + "spm": { + "source": "IRAF Observatory Database", + "elevation": 2830, + "name": "Observatorio Astronomico Nacional, San Pedro Martir", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 31.029166666666665, + "elevation_unit": "meter", + "longitude": 244.51305555555555, + "timezone": "US/Pacific", + "aliases": ["Observatorio Astronomico Nacional, San Pedro Martir"] + }, + "mro": { + "aliases": ["Manastash Ridge Observatory"], + "elevation": 1198, + "elevation_unit": "meter", + "latitude": 46.9528, + "latitude_unit": "degree", + "longitude": -120.7278, + "timezone": "US/Pacific", + "longitude_unit": "degree", + "name": "Manastash Ridge Observatory", + "source": "MRO website" + }, + "haleakala": { + "source": "Ifa/University of Hawaii website", + "elevation": 3048, + "name": "Haleakala Observatories", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 20.71552, + "elevation_unit": "meter", + "longitude": 203.831, + "timezone": "US/Hawaii", + "aliases": ["Haleakala Observatories"] + }, + "gemini_south": { + "source": "CTIO Website", + "elevation": 2750, + "name": "Gemini South", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -30.24074166666667, + "elevation_unit": "meter", + "longitude": 289.2633166666667, + "timezone": "Chile/Continental", + "aliases": ["Cerro Pachon", "Gemini South", "gems"] + }, + "gemini_north": { + "source": "CTIO Website", + "elevation": 4213, + "name": "Gemini North", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.823801447222223, + "elevation_unit": "meter", + "longitude": -155.4690467527778, + "timezone": "US/Hawaii", + "aliases": ["gemn"] + }, + "rubin": { + "source": "https://arxiv.org/abs/1210.1616", + "elevation": 2662.75, + "name": "Rubin Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -30.244633333333333, + "elevation_unit": "meter", + "longitude": -70.74941666666666, + "timezone": "Chile/Continental", + "aliases": ["LSST", "LSST 8.4m", "Rubin"] + }, + "rubin_aux": { + "source": "https://arxiv.org/abs/1210.1616", + "elevation": 2647, + "name": "Rubin AuxTel", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -30.244797222222225, + "elevation_unit": "meter", + "longitude": -70.74772222222222, + "timezone": "Chile/Continental", + "aliases": ["LSST 1.4m", "LSST AuxTel"] + }, + "irtf": { + "source": "IRTF Website", + "elevation": 4168, + "name": "NASA Infrared Telescope Facility", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.826218316666665, + "elevation_unit": "meter", + "longitude": -155.4719987888889, + "timezone": "US/Hawaii", + "aliases": [""] + }, + "lbt": { + "source": "LBT website", + "elevation": 2902, + "name": "Large Binocular Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 32.7016, + "elevation_unit": "meter", + "longitude": 250.1281, + "timezone": "US/Mountain", + "aliases": [ + "Large Binocular Telescope", + "Mount Graham International Observatory", + "Mt Graham" + ] + }, + "salt": { + "source": "SALT website & google maps", + "elevation": 1798, + "name": "Southern African Large Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -32.375823, + "elevation_unit": "meter", + "longitude": 20.810808, + "timezone": "Africa/Johannesburg", + "aliases": [ + "SALT", + "Southern African Large Telescope", + "SAAO", + "Sutherland" + ] + }, + "srt": { + "source": "SRT single dish tools & pulsar software such as TEMPO2 and PINT. Converted through EarthLocation(4865182.7660, 791922.6890, 4035137.1740, unit=u.m).to_geodetic() on 2022-11-21", + "elevation": 671.6665, + "name": "Sardinia Radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 39.49307239, + "elevation_unit": "meter", + "longitude": 9.24515124, + "timezone": "Europe/Rome", + "aliases": ["SRT"] + }, + "medicina": { + "source": "Medicina Radio Telescope website & google maps", + "elevation": 25.0, + "name": "Medicina Radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 44.5205, + "elevation_unit": "meter", + "longitude": 11.6469, + "timezone": "Europe/Rome", + "aliases": ["Medicina Dish", "Medicina"] + }, + "noto": { + "source": "Noto Radio Telescope website & google maps", + "elevation": 30.0, + "name": "Noto Radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 36.87585, + "elevation_unit": "meter", + "longitude": 14.9889, + "timezone": "Europe/Rome", + "aliases": ["Noto"] + }, + "ohp": { + "source": "OHP website", + "elevation": 650, + "name": "Observatoire de Haute Provence", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 43.93083333333333, + "elevation_unit": "meter", + "longitude": 5.713333333333333, + "timezone": "Europe/Paris", + "aliases": [""] + }, + "sirene": { + "source": "SIRENE website", + "elevation": 1100, + "name": "Observatoire SIRENE", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 44.0, + "elevation_unit": "meter", + "longitude": 5.486944444444444, + "timezone": "Europe/Paris", + "aliases": [""] + }, + "dct": { + "source": "Site GPS on 20130708 UT as recorded in observatory wiki", + "elevation": 2337.0, + "name": "Lowell Discovery Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 34.744305, + "elevation_unit": "meter", + "longitude": 248.577485, + "timezone": "US/Mountain", + "aliases": [ + "DCT", + "Discovery Channel Telescope", + "Happy Jack", + "LDT", + "ldt", + "Lowell Discovery Telescope" + ] + }, + "vla": { + "aliases": ["Very Large Array", "Jansky Very Large Array"], + "elevation": 2124, + "elevation_unit": "meter", + "latitude": 34.07874916666667, + "latitude_unit": "degree", + "longitude": -107.61828305555555, + "longitude_unit": "degree", + "timezone": "US/Mountain", + "name": "vla", + "source": "https://en.wikipedia.org/wiki/Very_Large_Array" + }, + "alma": { + "aliases": ["Atacama Large Millimeter Array", "ALMA"], + "elevation": 5000, + "elevation_unit": "meter", + "latitude": -23.029, + "latitude_unit": "degree", + "longitude": -67.755, + "longitude_unit": "degree", + "timezone": "Chile/Continental", + "name": "alma", + "source": "https://almascience.eso.org/about-alma/alma-site" + }, + "tug": { + "source": "TUG Website: http://www.tug.tubitak.gov.tr/gozlemevi.php", + "elevation": 2500, + "name": "TUBITAK National Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 36.824166, + "elevation_unit": "meter", + "longitude": 30.335555, + "timezone": "Europe/Istanbul", + "aliases": ["TUG"] + }, + "gbt": { + "aliases": ["Green Bank Telescope", "GBT", "Green Bank Observatory"], + "elevation": 0, + "elevation_unit": "meter", + "latitude": 38.433056, + "latitude_unit": "degree", + "longitude": -79.839722, + "longitude_unit": "degree", + "timezone": "US/Eastern", + "elevation": 807, + "elevation_unit": "meter", + "name": "gbt", + "source": "https://en.wikipedia.org/wiki/Green_Bank_Telescope + Google Elevation service for elevation" + }, + "ao": { + "aliases": ["AO", "Arecibo Observatory", "Arecibo", "arecibo"], + "elevation": 497, + "elevation_unit": "meter", + "latitude": 18.34416667, + "latitude_unit": "degree", + "longitude": 293.24722222, + "longitude_unit": "degree", + "timezone": "America/Puerto_Rico", + "name": "Arecibo Observatory", + "source": "https://en.wikipedia.org/wiki/Arecibo_Observatory" + }, + "jcmt": { + "aliases": ["James Clerk Maxwell Telescope", "JCMT"], + "name": "James Clerk Maxwell Telescope", + "elevation": 4124.75, + "elevation_unit": "meter", + "latitude": 19.8228, + "latitude_unit": "degree", + "longitude": 204.523, + "longitude_unit": "degree", + "timezone": "US/Hawaii", + "source": "2007-04-11 GPS measurements by R. Tilanus (via Starlink/PAL)" + }, + "ukirt": { + "aliases": ["UKIRT", "United Kingdom Infrared Telescope"], + "name": "United Kingdom Infrared Telescope", + "elevation": 4198.5, + "elevation_unit": "meter", + "latitude": 19.8224306, + "latitude_unit": "degree", + "longitude": 204.529672, + "longitude_unit": "degree", + "timezone": "US/Hawaii", + "source": "IfA website via Starlink/PAL" + }, + "mwa": { + "aliases": ["MWA", "Murchison Widefield Array"], + "name": "Murchison Widefield Array", + "elevation": 377.83, + "elevation_unit": "meter", + "latitude": -26.7033194, + "latitude_unit": "degree", + "longitude": 116.67081524, + "longitude_unit": "degree", + "timezone": "Australia/Perth", + "source": "MWA website: http://mwatelescope.org/telescope" + }, + "iao": { + "source": "https://www.iiap.res.in/centers/iao?q=iao_site", + "elevation": 4500, + "name": "Indian Astronomical Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 32.779444444444444, + "elevation_unit": "meter", + "longitude": 78.96416666666667, + "timezone": "Asia/Kolkata", + "aliases": ["IAO"] + }, + "dkist": { + "name": "Daniel K. Inouye Solar Telescope", + "elevation": 3067, + "elevation_unit": "meter", + "latitude": 20.7067, + "latitude_unit": "degree", + "longitude": 203.7436, + "longitude_unit": "degree", + "timezone": "US/Hawaii", + "source": "DKIST website: https://www.nso.edu/telescopes/dki-solar-telescope/", + "aliases": ["DKIST", "ATST"] + }, + "bbso": { + "name": "Big Bear Solar Observatory", + "elevation": 2067, + "elevation_unit": "meter", + "latitude": 34.2583, + "latitude_unit": "degree", + "longitude": 243.0786, + "longitude_unit": "degree", + "timezone": "US/Pacific", + "aliases": ["BBSO", "NST"], + "source": "BBSO website: http://www.bbso.njit.edu/newinfo.html" + }, + "wiyn": { + "aliases": ["WIYN", "WIYN Observatory", "WIYN 3.5 m"], + "name": "WIYN Observatory", + "latitude": 31.95805, + "latitude_unit": "degree", + "longitude": -111.600619, + "longitude_unit": "degree", + "elevation": 2091.0, + "elevation_unit": "meter", + "timezone": "US/Arizona", + "source": "WIYN 3.5m TCS" + }, + "het": { + "aliases": ["HET", "Hobby Eberly Telescope"], + "name": "Hobby Eberly Telescope", + "latitude": 30.681389, + "latitude_unit": "degree", + "longitude": 255.985278, + "longitude_unit": "degree", + "elevation": 2026.0, + "elevation_unit": "meter", + "timezone": "US/Central", + "source": "Wikipedia" + }, + "spo": { + "aliases": ["SPO", "Sacramento Peak", "Sac Peak", "Sunspot"], + "name": "Sacramento Peak Observatory", + "elevation": 2800, + "elevation_unit": "meter", + "latitude": 32.7873, + "latitude_unit": "degree", + "longitude": 254.1795, + "longitude_unit": "degree", + "timezone": "US/Mountain", + "source": "NSO website: https://nsosp-dev.nso.edu/node/18" + }, + "teide": { + "name": "Observatorio del Teide, Tenerife", + "elevation": 2390, + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 28.3, + "elevation_unit": "meter", + "longitude": 343.490278, + "timezone": "Atlantic/Canary", + "source": "Instituto de Astrofisica de Canarias website (http://research.iac.es/OOCC/observatorio-del-teide/ot/)", + "aliases": ["Observatorio del Teide", "OT"] + }, + "CAHA": { + "name": "Centro Astronomico Hispano-Aleman, Almeria", + "elevation": 2168, + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 37.22361111, + "elevation_unit": "meter", + "longitude": 357.453889, + "timezone": "Europe/Madrid", + "source": "CAHA Wikipedia page (https://en.wikipedia.org/wiki/Calar_Alto_Observatory)", + "aliases": ["Observatorio de Calar Alto", "CAHA"] + }, + "OARMA": { + "name": "Observatorio Astronomico Ramon Maria Aller, Santiago de Compostela", + "elevation": 240, + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 42.87594, + "elevation_unit": "meter", + "longitude": 351.44455, + "timezone": "Europe/Madrid", + "source": "OpenStreetView", + "aliases": ["Observatorio Ramon Maria Aller", "OARMA"] + }, + "OAJ": { + "elevation": 1956, + "name": "Observatorio Astrofisico de Javalambre", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 40.04201944, + "elevation_unit": "meter", + "longitude": 358.98391667, + "timezone": "Europe/Madrid", + "source": "Google Earth", + "aliases": ["OAJ"] + }, + "OAO": { + "name": "Okayama Astrophysical Observatory", + "elevation": 370, + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 34.577075, + "elevation_unit": "meter", + "longitude": 133.5939556, + "timezone": "Asia/Tokyo", + "source": "OAO Website (http://www.oao.nao.ac.jp/en/telescope/abouttel188/)", + "aliases": ["OAO"] + }, + "TNO": { + "name": "Thai National Observatory", + "elevation": 2457, + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 18.59055556, + "elevation_unit": "meter", + "longitude": 98.48666667, + "timezone": "Asia/Bangkok", + "source": "TNO Wikipedia page (https://en.wikipedia.org/wiki/Thai_National_Observatory)", + "aliases": ["TNO"] + }, + "geo_600": { + "name": "GEO600 Gravitational Wave Detector", + "aliases": ["GEO", "GEO_600", "G1"], + "timezone": "Europe/Berlin", + "elevation": 114.42500305175781, + "elevation_unit": "meter", + "latitude": 52.24514666662807, + "latitude_unit": "degree", + "longitude": 9.807192777776013, + "longitude_unit": "degree", + "source": "LALSuite: https://git.ligo.org/lscsoft/lalsuite/-/blob/lalsuite-v6.70/lal/lib/tools/LALDetectors.h" + }, + "llo_4k": { + "name": "LIGO Livingston Observatory", + "aliases": ["LLO", "LLO_4k", "L1"], + "timezone": "US/Central", + "elevation": -6.573999881744385, + "elevation_unit": "meter", + "latitude": 30.562894333574896, + "latitude_unit": "degree", + "longitude": -90.77424038872107, + "longitude_unit": "degree", + "source": "LALSuite: https://git.ligo.org/lscsoft/lalsuite/-/blob/lalsuite-v6.70/lal/lib/tools/LALDetectors.h" + }, + "lho_4k": { + "name": "LIGO Hanford Observatory", + "aliases": ["LHO", "LHO_4k", "H1"], + "timezone": "US/Pacific", + "elevation": 142.5540008544922, + "elevation_unit": "meter", + "latitude": 46.45514666665509, + "latitude_unit": "degree", + "longitude": -119.40765713911102, + "longitude_unit": "degree", + "source": "LALSuite: https://git.ligo.org/lscsoft/lalsuite/-/blob/lalsuite-v6.70/lal/lib/tools/LALDetectors.h" + }, + "kagra": { + "name": "Kamioka Gravitational Wave Detector", + "aliases": ["KAGRA", "K1"], + "timezone": "Asia/Tokyo", + "elevation": 414.1809997558594, + "elevation_unit": "meter", + "latitude": 36.41186033946475, + "latitude_unit": "degree", + "longitude": 137.3059560115472, + "longitude_unit": "degree", + "source": "LALSuite: https://git.ligo.org/lscsoft/lalsuite/-/blob/lalsuite-v6.70/lal/lib/tools/LALDetectors.h" + }, + "virgo": { + "name": "Virgo Observatory", + "aliases": ["Virgo", "VIRGO", "V1"], + "timezone": "Europe/Rome", + "elevation": 51.88399887084961, + "elevation_unit": "meter", + "latitude": 43.631414472074304, + "latitude_unit": "degree", + "longitude": 10.504496611198473, + "longitude_unit": "degree", + "source": "LALSuite: https://git.ligo.org/lscsoft/lalsuite/-/blob/lalsuite-v6.70/lal/lib/tools/LALDetectors.h" + }, + "SuperK": { + "source": "SK detector paper, Fukuda et al. 2003", + "name": "Super-Kamiokande", + "elevation": 370, + "elevation_unit": "meter", + "latitude": 36.425722, + "latitude_unit": "degree", + "longitude": 137.310306, + "longitude_unit": "degree", + "timezone": "Asia/Tokyo", + "aliases": ["superk", "Superk", "superK"] + }, + "HyperK": { + "source": "Design Report, arXiv:1805.04163v2", + "name": "Hyper-Kamiokande", + "elevation": 514, + "elevation_unit": "meter", + "latitude": 36.355583, + "latitude_unit": "degree", + "longitude": 137.313639, + "longitude_unit": "degree", + "timezone": "Asia/Tokyo", + "aliases": ["hyperk", "Hyperk", "hyperK"] + }, + "ARCA": { + "source": "SNEWS 2.0 Collaboration", + "name": "Astroparticle Research with Cosmics in the Abyss", + "elevation": -3500, + "elevation_unit": "meter", + "latitude": 36.266667, + "latitude_unit": "degree", + "longitude": 16.1, + "longitude_unit": "degree", + "timezone": "Europe/Rome", + "aliases": ["arca", "KM3NeT ARCA", "km3net arca", "KM3NeT arca"] + }, + "ORCA": { + "source": "SNEWS 2.0 Collaboration", + "name": "Oscillation Research with Cosmics in the Abyss", + "elevation": -2450, + "elevation_unit": "meter", + "latitude": 42.8, + "latitude_unit": "degree", + "longitude": 6.033333, + "longitude_unit": "degree", + "timezone": "Europe/Paris", + "aliases": ["orca", "KM3NeT ORCA", "km3net orca", "KM3NeT orca"] + }, + "SNO+": { + "source": "SNEWS 2.0 Collaboration", + "name": "Sudbury Neutrino Observatory +", + "elevation": -1740, + "elevation_unit": "meter", + "latitude": 46.475, + "latitude_unit": "degree", + "longitude": -81.201111, + "longitude_unit": "degree", + "timezone": "America/New_York", + "aliases": ["sno+"] + }, + "HALO": { + "source": "SNEWS 2.0 Collaboration", + "name": "Helium And Lead Observatory", + "elevation": -1740, + "elevation_unit": "meter", + "latitude": 46.475, + "latitude_unit": "degree", + "longitude": -81.201111, + "longitude_unit": "degree", + "timezone": "America/New_York", + "aliases": ["halo"] + }, + "NOvA": { + "source": "SNEWS 2.0 Collaboration", + "name": "NuMI Off-axis νe Appearance", + "elevation": 367.6, + "elevation_unit": "meter", + "latitude": 48.378628828751324, + "latitude_unit": "degree", + "longitude": -92.83113454369206, + "longitude_unit": "degree", + "timezone": "America/Mexico_City", + "aliases": ["nova", "NOVA"] + }, + "IceCube": { + "source": "SNEWS 2.0 Collaboration", + "name": "IceCube Neutrino Observatory", + "elevation": 850, + "elevation_unit": "meter", + "latitude": -90.0, + "latitude_unit": "degree", + "longitude": 0.0, + "longitude_unit": "degree", + "timezone": "Pacific/Auckland", + "aliases": ["icecube", "ICECUBE"] + }, + "ovro": { + "name": "Owens Valley Radio Observatory", + "aliases": ["mma"], + "timezone": "US/Pacific", + "elevation": 1188.6, + "elevation_unit": "meter", + "latitude": 37.233386982, + "latitude_unit": "degree", + "longitude": -118.283405115, + "longitude_unit": "degree", + "source": "OVRO staff measurement of center of T using GNSS" + }, + "chime": { + "name": "Canadian Hydrogen Intensity Mapping Experiment", + "aliases": ["CHIME"], + "timezone": "America/Vancouver", + "elevation": 555.37238077, + "elevation_unit": "meter", + "latitude": 49.32070922, + "latitude_unit": "degree", + "longitude": -119.62367743, + "longitude_unit": "degree", + "source": "Journey to the center of CHIME, Calvin Leung, CHIME memo. Also, 2018 ApJ...863...48C." + }, + "drao": { + "source": "Geometry of the Penticton 25.6 m, John Galt, DRAO memo", + "elevation": 546.566, + "name": "Dominion Radio Astrophysical Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 49.3210230556, + "elevation_unit": "meter", + "longitude": 240.3810197222, + "timezone": "Canada/Pacific", + "aliases": [ + "Dominion Radio Astrophysical Observatory", + "DRAO", + "John Galt Telescope", + "DRAO 26m Telescope" + ] + }, + "meerkat": { + "elevation": 1086.599484882955, + "name": "MeerKAT", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -30.711055553291878, + "elevation_unit": "meter", + "longitude": 21.443888889697842, + "source": "MEERKAT, used in timing mode.\n\n The origin of this data is unknown but as of 2021 June 8 it agrees exactly with\n the values used by TEMPO and TEMPO2.\nvia PINT", + "timezone": "Africa/Johannesburg", + "aliases": ["MeerKAT", "MEERKAT"] + }, + "nancay": { + "elevation": 190.94343144560776, + "name": "Nancay Radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 47.373601708263124, + "elevation_unit": "meter", + "longitude": 2.1974784506622265, + "source": "The Nan\u00e7ay radio telescope.\n\n The origin of this data is unknown but as of 2021 June 8 it agrees exactly with\n the values used by TEMPO and TEMPO2.\nvia PINT", + "timezone": "Europe/Paris", + "aliases": ["NANCAY", "Nancay"] + }, + "gmrt": { + "elevation": 497.00082854780277, + "name": "Giant Metrewave Radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 19.09300278307055, + "elevation_unit": "meter", + "longitude": 74.05656115769753, + "source": "The Giant Metrewave Radio Telescope.\n\n The origin of this data is unknown but as of 2021 June 8 it agrees exactly with\n the values used by TEMPO and TEMPO2.\nvia PINT", + "timezone": "Asia/Calcutta", + "aliases": ["GMRT"] + }, + "fast": { + "elevation": 1109.1023222846998, + "name": "Five-hundred-meter Aperture Spherical radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 25.652946164090935, + "elevation_unit": "meter", + "longitude": 106.85666641926392, + "source": "The FAST radio telescope in China.\n\n Origin of this data is unknown but as of 2021 June 8 it agrees exactly with the\n TEMPO value and disagrees by about 17 km with the TEMPO2 value.\nvia PINT", + "timezone": "Asia/Shanghai", + "aliases": ["FAST"] + }, + "lwa1": { + "elevation": 2126.996641832515, + "name": "Long Wavelength Array 1", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 34.068899974626035, + "elevation_unit": "meter", + "longitude": -107.62767002113483, + "source": "The LWA (long wavelength array, in New Mexico).\n\n Origin of this data is unknown but as of 2021 June 8 this value agrees exactly with\n the value used by TEMPO2 but disagrees with the value used by TEMPO by about 125 m.\nvia PINT", + "timezone": "US/Mountain", + "aliases": ["LWA1"] + }, + "lofar": { + "elevation": 49.35002954676998, + "name": "Low-Frequency Array", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 52.91511896798418, + "elevation_unit": "meter", + "longitude": 6.869832836200029, + "source": "The Dutch low-frequency array LOFAR.\n\n Note that other TEMPO codes have been used for this telescope.\n\n Imported from TEMPO2 observatories.dat 2021 June 7.\nvia PINT", + "timezone": "Europe/Amsterdam", + "aliases": ["LOFAR"] + }, + "omm": { + "aliases": [ + "OMM", + "Mont Mégantic Observatory", + "Observatoire du Mont Mégantic" + ], + "name": "Observatoire du Mont Mégantic", + "elevation": 1108, + "elevation_unit": "meter", + "latitude": 45.4557111111, + "latitude_unit": "degree", + "longitude": -71.1525, + "longitude_unit": "degree", + "timezone": "Canada/Eastern", + "source": "Google Maps" + }, + "parkes": { + "elevation": 414.7597497078894, + "name": "Parkes", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -32.99840639865134, + "elevation_unit": "meter", + "longitude": 148.2635100132104, + "source": "The Parkes radio telescope.\n\n The origin of this data is unknown but as of 2021 June 8 it agrees exactly with\n the values used by TEMPO and TEMPO2.\nvia PINT", + "timezone": "Australia/Sydney", + "aliases": ["Parkes", "Murriyang"] + }, + "effelsberg": { + "elevation": 416.7160563557801, + "name": "Effelsberg 100-m Radio Telescope", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 50.52483588054502, + "elevation_unit": "meter", + "longitude": 6.8836164652709835, + "source": "The Effelsberg radio telescope.\n\n These are the coordinates used for VLBI as of March 2020 (MJD 58919). They are based on\n a fiducial position at MJD 56658 plus a (continental) drift velocity of\n [-0.0144, 0.0167, 0.0106] m/yr. This data was obtained from Ben Perera in September 2021.\nvia PINT", + "timezone": "Europe/Berlin", + "aliases": ["Effelsberg"] + }, + "mars_hill": { + "source": "Lowell Observatory Staff", + "elevation": 2195, + "name": "Lowell Observatory - Mars Hill", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": 35.202875, + "elevation_unit": "meter", + "longitude": -111.664781, + "timezone": "US/Mountain", + "aliases": ["Mars Hill", "lo-mh", "mh"] + }, + "askap": { + "aliases": ["ASKAP", "Australian Square Kilometre Array Pathfinder"], + "name": "Australian Square Kilometre Array Pathfinder", + "elevation": 360.99, + "elevation_unit": "meter", + "latitude": -26.697, + "latitude_unit": "degree", + "longitude": 116.631424, + "longitude_unit": "degree", + "timezone": "Australia/Perth", + "source": "ASKAP Science Observation Guide, Version 1.1 https://confluence.csiro.au/display/askapsst/?preview=/733676544/887260100/ASKAP_sci_obs_guide.pdf" + }, + "Otehiwai Observatory": { + "elevation": 1029, + "name": "Otehiwai Observatory", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -43.98574085, + "elevation_unit": "meter", + "longitude": 170.46506246, + "timezone": "NZ", + "source": "https://www.geodesy.linz.govt.nz/gdb/index.cgi?code=6702", + "aliases": ["Mt John", "MJO", "MOA", "Otehiwai"] + }, + "winer": { + "name": "Winer Observatory", + "elevation": 1515.7, + "elevation_unit": "meter", + "latitude": 31.665578, + "latitude_unit": "degree", + "longitude": -110.601783, + "longitude_unit": "degree", + "timezone": "US/Arizona", + "source": "Winer Observatory website http://www.winer.org/", + "aliases": ["Winer", "Winer Observatory"] + }, + "wise": { + "name": "Wise Observatory", + "elevation": 875, + "elevation_unit": "meter", + "latitude": 30.59739, + "latitude_unit": "degree", + "longitude": 34.76218, + "longitude_unit": "degree", + "timezone": "Asia/Tel_Aviv", + "source": "Wise Observatory One Meter Telescope Manual http://wise-obs.tau.ac.il/observations/Man/wise_man.pdf", + "aliases": ["wise", "Wise Observatory"] + }, + "oca": { + "source": "https://araucaria.camk.edu.pl/index.php/observatory-cerro-armazones/", + "elevation": 2817, + "name": "Observatorio Cerro Armazones", + "longitude_unit": "degree", + "latitude_unit": "degree", + "latitude": -24.59867, + "elevation_unit": "meter", + "longitude": -70.20128, + "timezone": "Chile/Continental", + "aliases": ["OCA", "Cerro Armazones Observatory"] + } +} diff --git a/tests/handlers/astroplan_test.py b/tests/handlers/astroplan_test.py new file mode 100644 index 0000000..40fb704 --- /dev/null +++ b/tests/handlers/astroplan_test.py @@ -0,0 +1,76 @@ +"""Tests for the astroplan router.""" + +from __future__ import annotations + +import pytest +from httpx import AsyncClient + +from fastapibootcamp.config import config + + +@pytest.mark.asyncio +async def test_get_observer_rubin(client: AsyncClient) -> None: + """Test ``GET /astroplan/observers/rubin``.""" + path = f"{config.path_prefix}/astroplan/observers/rubin" + response = await client.get(path) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Rubin Observatory" + assert data["id"] == "rubin" + assert data["self_url"].endswith(path) + + +@pytest.mark.asyncio +async def test_get_observer_not_found(client: AsyncClient) -> None: + """Test ``GET /astroplan/observers/not-a-site``.""" + path = f"{config.path_prefix}/astroplan/observers/not-a-site" + response = await client.get(path) + assert response.status_code == 404 + data = response.json() + assert data["detail"][0]["type"] == "unknown_observer" + assert data["detail"][0]["loc"] == ["path", "observer_id"] + + +@pytest.mark.asyncio +async def test_get_observers_rubin(client: AsyncClient) -> None: + """Test finding observing sites with Rubin in name.""" + response = await client.get( + f"{config.path_prefix}/astroplan/observers?name=rubin" + ) + assert response.status_code == 200 + data = response.json() + assert len(data) == 2 + assert data[0]["name"] == "Rubin Observatory" + assert data[1]["name"] == "Rubin AuxTel" + + +@pytest.mark.asyncio +async def test_get_observers_with_aliases(client: AsyncClient) -> None: + """Test finding observing sites with LSST in their aliases.""" + response = await client.get( + f"{config.path_prefix}/astroplan/observers?name=lsst" + ) + assert response.status_code == 200 + data = response.json() + assert len(data) == 2 + assert data[0]["name"] == "Rubin Observatory" + assert data[1]["name"] == "Rubin AuxTel" + + +@pytest.mark.asyncio +async def test_rubin_lmc_observability(client: AsyncClient) -> None: + """Test ``POST /astroplan/observers/rubin/observable``.""" + path = f"{config.path_prefix}/astroplan/observers/rubin/observable" + response = await client.post( + path, + json={ + "ra": "05h23m34.6s", + "dec": "-69d45m22s", + "time": "2024-04-24T00:00:00Z", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["is_night"] is True + assert data["above_horizon"] is True + assert data["observer_url"].endswith("/astroplan/observers/rubin") diff --git a/tox.ini b/tox.ini index c81f7d1..19cb723 100644 --- a/tox.ini +++ b/tox.ini @@ -33,4 +33,6 @@ commands = pre-commit run --all-files [testenv:run] description = Run the development server with auto-reload for code changes. usedevelop = true +setenv = + FASTAPI_BOOTCAMP_CLEAR_IERS_ON_STARTUP = False commands = uvicorn fastapibootcamp.main:app --reload