diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d302149..239015a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-toml - id: check-yaml - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.5 + rev: v0.6.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/Dockerfile b/Dockerfile index 8073cb0..7354570 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,27 @@ -# This Dockerfile has four stages: +# This Dockerfile has three stages: # # base-image # Updates the base Python image with security patches and common system # packages. This image becomes the base of all other images. -# dependencies-image -# Installs third-party dependencies (requirements/main.txt) into a virtual -# environment. This virtual environment is ideal for copying across build -# stages. # install-image -# Installs the app into the virtual environment. +# Installs third-party dependencies (requirements/main.txt) and the +# application into a virtual environment. This virtual environment is +# ideal for copying across build stages. # runtime-image # - Copies the virtual environment into place. # - Runs a non-root user. # - Sets up the entrypoint and port. -FROM python:3.12.7-slim-bookworm as base-image +FROM python:3.12.7-slim-bookworm AS base-image # Update system packages COPY scripts/install-base-packages.sh . RUN ./install-base-packages.sh && rm ./install-base-packages.sh -FROM base-image AS dependencies-image +FROM base-image AS install-image + +# Install uv. +COPY --from=ghcr.io/astral-sh/uv:0.4.9 /uv /bin/uv # Install system packages only needed for building dependencies. COPY scripts/install-dependency-packages.sh . @@ -29,23 +30,19 @@ RUN ./install-dependency-packages.sh # Create a Python virtual environment ENV VIRTUAL_ENV=/opt/venv RUN python -m venv $VIRTUAL_ENV + # Make sure we use the virtualenv ENV PATH="$VIRTUAL_ENV/bin:$PATH" -# Put the latest pip and setuptools in the virtualenv -RUN pip install --upgrade --no-cache-dir pip setuptools wheel # Install the app's Python runtime dependencies COPY requirements/main.txt ./requirements.txt -RUN pip install --quiet --no-cache-dir -r requirements.txt - -FROM dependencies-image AS install-image - -# Use the virtualenv -ENV PATH="/opt/venv/bin:$PATH" +RUN uv pip install --compile-bytecode --verify-hashes --no-cache \ + -r requirements.txt +# Install the application. COPY . /workdir WORKDIR /workdir -RUN pip install --no-cache-dir . +RUN uv pip install --compile-bytecode --no-cache . FROM base-image AS runtime-image @@ -55,6 +52,8 @@ RUN useradd --create-home appuser # Copy the virtualenv COPY --from=install-image /opt/venv /opt/venv +WORKDIR /app + # Make sure we use the virtualenv ENV PATH="/opt/venv/bin:$PATH" @@ -65,4 +64,4 @@ USER appuser EXPOSE 8080 # Run the application. -CMD ["uvicorn", "vosiav2.main:app", "--host", "0.0.0.0", "--port", "8080"] +CMD ["uvicorn", "sia.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/Makefile b/Makefile index 07422a7..84e45fc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: help help: - @echo "Make targets for vo-siav2" + @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" @@ -29,20 +29,9 @@ update-deps: pip install --upgrade uv uv pip install --upgrade pre-commit pre-commit autoupdate - uv pip compile --upgrade --generate-hashes \ + uv pip compile --upgrade --universal --generate-hashes \ --output-file requirements/main.txt requirements/main.in - uv pip compile --upgrade --generate-hashes \ + uv pip compile --upgrade --universal --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 -update-deps-no-hashes: - pip install --upgrade uv - uv pip compile --upgrade \ - --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 + uv pip compile --upgrade --universal --generate-hashes \ + --output-file requirements/tox.txt requirements/tox.in \ No newline at end of file diff --git a/README.md b/README.md index 1962e3f..b4ce8ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,17 @@ -# vo-siav2 +# SIA -Rubin Observatory SIAV2 implementation over Butler -Learn more at https://vo-siav2.lsst.io +SIA is an implementation of the IVOA [Simple Image Access v2](https://www.ivoa.net/documents/SIA/20150610/PR-SIA-2.0-20150610.pdf) protocol as a [FastAPI](https://fastapi.tiangolo.com/) web service, designed to be deployed as part of the Rubin Science Platform. + +The default configuration uses the [dax_obscore](https://github.com/lsst-dm/dax_obscore) package and interacts with a [Butler](https://github.com/lsst/daf_butler) repository to find images matching specific criteria. + + +While the current release supports both remote and direct (local) Butler repositories, our primary focus has been on the Remote Butler, resulting in more mature support for this option. + +Query results are streamed to the user as VOTable responses, which is currently the only supported format. + +The application expects as part of the configuration a list of Butler Data Collections, each of which expects a number of attributes which define how to access the repository. + +The system architecture & design considerations have been documented in https://github.com/lsst-sqre/sqr-095. + +See [CHANGELOG.md](https://github.com/lsst-sqre/sia/blob/main/CHANGELOG.md) for the change history of sia. -vo-siav2 is developed with [FastAPI](https://fastapi.tiangolo.com) and [Safir](https://safir.lsst.io). diff --git a/pyproject.toml b/pyproject.toml index 0beab2e..fcb6bc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] # https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ -name = "vo-siav2" -description = "Rubin Observatory SIAV2 implementation over Butler" +name = "sia" +description = "Rubin Observatory SIA implementation over Butler" license = { file = "LICENSE" } readme = "README.md" keywords = ["rubin", "lsst"] @@ -23,11 +23,11 @@ dependencies = [] dynamic = ["version"] [project.scripts] -vo-siav2 = "vosiav2.cli:main" +sia = "sia.cli:main" [project.urls] -Homepage = "https://vo-siav2.lsst.io" -Source = "https://github.com/lsst-sqre/vo-siav2" +Homepage = "https://sia.lsst.io" +Source = "https://github.com/lsst-sqre/sia" [build-system] requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=6.2"] @@ -35,10 +35,13 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] +[tool.setuptools.package-data] +"sia" = ["templates/*.xml"] + [tool.coverage.run] parallel = true branch = true -source = ["vosiav2"] +source = ["sia"] [tool.coverage.paths] source = ["src", ".tox/*/site-packages"] @@ -62,7 +65,7 @@ disallow_untyped_defs = true disallow_incomplete_defs = true ignore_missing_imports = true local_partial_types = true -plugins = ["pydantic.mypy"] +plugins = ["pydantic.mypy","pydantic_xml.mypy"] no_implicit_reexport = true show_error_codes = true strict_equality = true @@ -87,6 +90,7 @@ asyncio_mode = "strict" # with complex data structures rather than only the assert message) in files # listed in python_files. python_files = ["tests/*.py", "tests/*/*.py"] +asyncio_default_fixture_loop_scope = "function" # Use the generic Ruff configuration in ruff.toml and extend it with only # project-specific settings. Add a [tool.ruff.lint.extend-per-file-ignores] @@ -95,7 +99,7 @@ python_files = ["tests/*.py", "tests/*/*.py"] extend = "ruff-shared.toml" [tool.ruff.lint.isort] -known-first-party = ["vosiav2", "tests"] +known-first-party = ["sia", "tests"] split-on-trailing-comma = false [tool.scriv] diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..2408d58 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,526 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --output-file requirements/dev.txt requirements/dev.in +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via + # -c requirements/main.txt + # pydantic +anyio==4.6.2.post1 \ + --hash=sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c \ + --hash=sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d + # via + # -c requirements/main.txt + # httpx +asgi-lifespan==2.1.0 \ + --hash=sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308 \ + --hash=sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f + # via -r requirements/dev.in +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 + # via scriv +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 + # via + # -c requirements/main.txt + # httpcore + # httpx + # requests +charset-normalizer==3.4.0 \ + --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ + --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ + --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ + --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ + --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ + --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ + --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ + --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ + --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ + --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ + --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ + --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ + --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ + --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ + --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ + --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ + --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ + --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ + --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ + --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ + --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ + --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ + --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ + --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ + --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ + --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ + --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ + --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ + --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ + --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ + --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ + --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ + --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ + --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ + --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ + --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ + --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ + --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ + --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ + --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ + --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ + --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ + --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ + --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ + --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ + --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ + --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ + --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ + --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ + --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ + --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ + --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ + --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ + --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ + --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ + --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ + --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ + --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ + --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ + --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ + --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ + --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ + --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ + --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ + --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ + --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ + --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ + --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ + --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ + --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ + --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ + --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ + --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ + --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ + --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ + --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ + --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ + --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ + --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ + --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ + --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ + --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ + --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ + --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ + --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ + --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ + --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ + --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ + --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ + --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ + --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ + --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ + --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ + --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ + --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ + --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ + --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ + --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ + --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ + --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ + --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ + --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ + --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ + --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ + --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 + # via + # -c requirements/main.txt + # requests +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via + # -c requirements/main.txt + # click-log + # scriv +click-log==0.4.0 \ + --hash=sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975 \ + --hash=sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756 + # via scriv +colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # -c requirements/main.txt + # click + # pytest +coverage==7.6.3 \ + --hash=sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6 \ + --hash=sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2 \ + --hash=sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba \ + --hash=sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb \ + --hash=sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6 \ + --hash=sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4 \ + --hash=sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0 \ + --hash=sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6 \ + --hash=sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990 \ + --hash=sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3 \ + --hash=sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43 \ + --hash=sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175 \ + --hash=sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a \ + --hash=sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6 \ + --hash=sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97 \ + --hash=sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b \ + --hash=sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e \ + --hash=sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39 \ + --hash=sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd \ + --hash=sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d \ + --hash=sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f \ + --hash=sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc \ + --hash=sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976 \ + --hash=sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549 \ + --hash=sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c \ + --hash=sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5 \ + --hash=sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4 \ + --hash=sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b \ + --hash=sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e \ + --hash=sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3 \ + --hash=sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6 \ + --hash=sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e \ + --hash=sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929 \ + --hash=sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234 \ + --hash=sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13 \ + --hash=sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007 \ + --hash=sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3 \ + --hash=sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167 \ + --hash=sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d \ + --hash=sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d \ + --hash=sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40 \ + --hash=sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181 \ + --hash=sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054 \ + --hash=sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd \ + --hash=sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2 \ + --hash=sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91 \ + --hash=sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3 \ + --hash=sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b \ + --hash=sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38 \ + --hash=sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd \ + --hash=sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f \ + --hash=sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2 \ + --hash=sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba \ + --hash=sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f \ + --hash=sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83 \ + --hash=sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce \ + --hash=sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38 \ + --hash=sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c \ + --hash=sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f \ + --hash=sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21 \ + --hash=sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4 \ + --hash=sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92 + # via + # -r requirements/dev.in + # pytest-cov +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via + # -c requirements/main.txt + # httpcore +httpcore==1.0.6 \ + --hash=sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f \ + --hash=sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f + # via + # -c requirements/main.txt + # httpx +httpx==0.27.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 + # via + # -c requirements/main.txt + # -r requirements/dev.in +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # -c requirements/main.txt + # anyio + # httpx + # requests +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d + # via + # -c requirements/main.txt + # scriv +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via scriv +markupsafe==3.0.1 \ + --hash=sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396 \ + --hash=sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38 \ + --hash=sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a \ + --hash=sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8 \ + --hash=sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b \ + --hash=sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad \ + --hash=sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a \ + --hash=sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a \ + --hash=sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da \ + --hash=sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6 \ + --hash=sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8 \ + --hash=sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344 \ + --hash=sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a \ + --hash=sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8 \ + --hash=sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5 \ + --hash=sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7 \ + --hash=sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170 \ + --hash=sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132 \ + --hash=sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9 \ + --hash=sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd \ + --hash=sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9 \ + --hash=sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346 \ + --hash=sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc \ + --hash=sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589 \ + --hash=sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5 \ + --hash=sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915 \ + --hash=sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295 \ + --hash=sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453 \ + --hash=sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea \ + --hash=sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b \ + --hash=sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d \ + --hash=sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b \ + --hash=sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4 \ + --hash=sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b \ + --hash=sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7 \ + --hash=sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf \ + --hash=sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f \ + --hash=sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91 \ + --hash=sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd \ + --hash=sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50 \ + --hash=sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b \ + --hash=sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583 \ + --hash=sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a \ + --hash=sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984 \ + --hash=sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c \ + --hash=sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c \ + --hash=sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25 \ + --hash=sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa \ + --hash=sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4 \ + --hash=sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3 \ + --hash=sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97 \ + --hash=sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1 \ + --hash=sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd \ + --hash=sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772 \ + --hash=sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a \ + --hash=sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729 \ + --hash=sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca \ + --hash=sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6 \ + --hash=sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635 \ + --hash=sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b \ + --hash=sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f + # via + # -c requirements/main.txt + # jinja2 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mypy==1.12.0 \ + --hash=sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a \ + --hash=sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa \ + --hash=sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff \ + --hash=sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9 \ + --hash=sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57 \ + --hash=sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3 \ + --hash=sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b \ + --hash=sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721 \ + --hash=sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed \ + --hash=sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893 \ + --hash=sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b \ + --hash=sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7 \ + --hash=sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521 \ + --hash=sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9 \ + --hash=sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0 \ + --hash=sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d \ + --hash=sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469 \ + --hash=sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8 \ + --hash=sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e \ + --hash=sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1 \ + --hash=sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164 \ + --hash=sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475 \ + --hash=sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e \ + --hash=sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309 \ + --hash=sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7 \ + --hash=sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601 \ + --hash=sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e \ + --hash=sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0 \ + --hash=sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642 \ + --hash=sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f \ + --hash=sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f \ + --hash=sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266 + # via -r requirements/dev.in +mypy-extensions==1.0.0 \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 + # via mypy +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via + # -c requirements/main.txt + # pytest +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest +pydantic==2.9.2 \ + --hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \ + --hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12 + # via + # -c requirements/main.txt + # -r requirements/dev.in +pydantic-core==2.23.4 \ + --hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \ + --hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \ + --hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \ + --hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \ + --hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \ + --hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \ + --hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \ + --hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \ + --hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \ + --hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \ + --hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \ + --hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \ + --hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \ + --hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \ + --hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \ + --hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \ + --hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \ + --hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \ + --hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \ + --hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \ + --hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \ + --hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \ + --hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \ + --hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \ + --hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \ + --hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \ + --hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \ + --hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \ + --hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \ + --hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \ + --hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \ + --hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \ + --hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \ + --hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \ + --hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \ + --hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \ + --hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \ + --hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \ + --hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \ + --hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \ + --hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \ + --hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \ + --hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \ + --hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \ + --hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \ + --hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \ + --hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \ + --hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \ + --hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \ + --hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \ + --hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \ + --hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \ + --hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \ + --hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \ + --hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \ + --hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \ + --hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \ + --hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \ + --hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \ + --hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \ + --hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \ + --hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \ + --hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \ + --hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \ + --hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \ + --hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \ + --hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \ + --hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \ + --hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \ + --hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \ + --hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \ + --hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \ + --hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \ + --hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \ + --hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \ + --hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \ + --hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \ + --hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \ + --hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \ + --hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \ + --hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \ + --hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \ + --hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \ + --hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \ + --hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \ + --hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \ + --hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \ + --hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \ + --hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607 + # via + # -c requirements/main.txt + # pydantic +pytest==8.3.3 \ + --hash=sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181 \ + --hash=sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2 + # via + # -r requirements/dev.in + # pytest-asyncio + # pytest-cov +pytest-asyncio==0.24.0 \ + --hash=sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b \ + --hash=sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276 + # via -r requirements/dev.in +pytest-cov==5.0.0 \ + --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \ + --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857 + # via -r requirements/dev.in +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # -c requirements/main.txt + # scriv +scriv==1.5.1 \ + --hash=sha256:30ae9ff8d144f8e0cf394c4e1d379542f1b3823767642955b54ec40dc00b32b6 \ + --hash=sha256:a3adc657733b4124fcb54527a5f3daab0d3c300de82d0fd2b9b297b243151b78 + # via -r requirements/dev.in +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # -c requirements/main.txt + # anyio + # asgi-lifespan + # httpx +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # -c requirements/main.txt + # mypy + # pydantic + # pydantic-core +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 + # via + # -c requirements/main.txt + # requests diff --git a/requirements/main.in b/requirements/main.in index 985d2fe..d14bae4 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -11,8 +11,20 @@ fastapi starlette uvicorn[standard] +python-multipart # Other dependencies. +psycopg2-binary +boto3 +google-cloud-storage +requests +jinja2 pydantic pydantic-settings safir>=5 +numpy +astropy +vo-models +defusedxml +lsst-daf-butler[postgres,remote] +lsst-dax-obscore @ git+https://github.com/lsst-dm/dax_obscore@main#egg=lsst-dax-obscore diff --git a/requirements/main.txt b/requirements/main.txt new file mode 100644 index 0000000..32913cf --- /dev/null +++ b/requirements/main.txt @@ -0,0 +1,1675 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --output-file requirements/main.txt requirements/main.in +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.6.2.post1 \ + --hash=sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c \ + --hash=sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d + # via + # httpx + # starlette + # watchfiles +astropy==6.1.4 \ + --hash=sha256:024cbba2b20d6abf50395669a010b1c026a0b966ba2292addd354163b0b44e55 \ + --hash=sha256:048ae0883db33f94dea03800211d2e4a0aacaf8d0f0640196815d9d1298b7449 \ + --hash=sha256:22cc40cb137bbd777389d96871d84438537d01e1cd1dd1fc3dca374b4ece3200 \ + --hash=sha256:2491dc9a8f9c046b808995b2e3dc9ad8349c83042729b26c7907b5f7fbc53fd4 \ + --hash=sha256:26c6cc099a312aaf0e473a4ea2ac4279f79ef53c8413e8259758d0f5930b4745 \ + --hash=sha256:27496654ae2e672fa13eee9200caa776f47fbdce9ece55adcfe1d7e37e80341e \ + --hash=sha256:361558e2b093a99bebe69f1fd47fac86a192607a4c16ed39ba0a800b2ab60c34 \ + --hash=sha256:50ab8d8097df76e33b56ab429d07240df6f273ccd267949ff99f8df79c7fcc42 \ + --hash=sha256:52683072d106162ca124e09f2f132de58ca9756e04c25ebd45d56cbdd6feb8f7 \ + --hash=sha256:59576f354772c448300438acc910b5a37b9d15ddfaa5c942746a7253a6c7765f \ + --hash=sha256:601d8a9a8d44f45064d2d8cc963cecf3949d3fbc10c9b6412c241cdc0c686b2c \ + --hash=sha256:7954cdfea00445ed186431888ec8ab12d9e3adfdf316038c44009f57438e6389 \ + --hash=sha256:81152825e80a03562cf2b95164c0ae7f56cd3521208a13221b3afa683c1469a6 \ + --hash=sha256:86e045e7ddfbfc500d015a9a6603be446c492c68fc019e69274b26e098aae50c \ + --hash=sha256:9d4d10d964bfd110751fd47132af5227807d2beca85669b3d00d29cf5bae167d \ + --hash=sha256:a1598720ac43875e602f1fe9766133ab67d50164047b5ed18f5e35d2fa69e914 \ + --hash=sha256:aeec3aa6c12f613b2609dfbf9bac355b5e64446e67643637ee449bdcf583f5e2 \ + --hash=sha256:af49e0a80ee6243727f449a6d8c99e711b2a4fdd1701d92c42964cd3e01a3490 \ + --hash=sha256:b23cb881cf1fa0795b92ab2d86b04339a0a38b336e3a391fd050b6caf695b01b \ + --hash=sha256:b28752d9515dac72358bce58214ef4bae89bfc4af548c98f6052ff91b38e93ee \ + --hash=sha256:b2fa9e1031eb83bee2d0b29f3e4d85a9d8b38ecb1b759a0ffbb8145fe8685d0e \ + --hash=sha256:c0298f0e0d57cc61915a4438d54e47b4517ff6c7c5144ae4679077cfb0826d0b \ + --hash=sha256:cf4ce31973dd18da522bb8f0fac91685ab1fcf21ca81251c79b09ef27d2b0d8f \ + --hash=sha256:cfc23b9e5e899f66f1377dc1116c247e6c7d0f92623ca115ad084a297414de03 \ + --hash=sha256:d3085e201e9d4bd223a543f7e2c0810dc873ea6a81bbdf06b5a987aa10e09bce \ + --hash=sha256:dbd7addf8c79d50e18eeef6c85aee7dd009f2e80756bc4d9b866592eeaca3577 \ + --hash=sha256:ec13a67ffac3d2b79ab2b725b16943133951cdd6833ab8941db798bc4de2da49 \ + --hash=sha256:f1ed2dfb1551b2f140ca636c0156ab6a6a13f4202b7f3bc06fa739058f311d18 \ + --hash=sha256:fa1a48e1dbd8072b3ef8a94334bc5344bf33bfd2b1b0236ca02eaa2cc3ca4b90 + # via + # -r requirements/main.in + # lsst-daf-butler + # lsst-felis + # lsst-resources + # lsst-utils +astropy-iers-data==0.2024.10.14.0.32.55 \ + --hash=sha256:1a7c8e12bfe2af5605fd46dd9f8cb7fc9e9a78328cd231fdf6753b1814efe597 \ + --hash=sha256:efe7e5b26fa064df08fcda528ea910c79137625208ccc4073addf4f0eb9289a0 + # via astropy +boto3==1.35.42 \ + --hash=sha256:a5b00f8b82dce62870759f04861747944da834d64a64355970120c475efdafc0 \ + --hash=sha256:e1f36f8be453505cebcc3da178ea081b2a06c0e5e1cdee774f1067599b8d9c3e + # via -r requirements/main.in +botocore==1.35.42 \ + --hash=sha256:05af0bb8b9cea7ce7bc589c332348d338a21b784e9d088a588fd10ec145007ff \ + --hash=sha256:af348636f73dc24b7e2dc760a34d08c8f2f94366e9b4c78d877307b128abecef + # via + # boto3 + # s3transfer +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a + # via google-auth +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 + # via + # httpcore + # httpx + # requests +cffi==1.17.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ + --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ + --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ + --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ + --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ + --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ + --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ + --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ + --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ + --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ + --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ + --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ + --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ + --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ + --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ + --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ + --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ + --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ + --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ + --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ + --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ + --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ + --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ + --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ + --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ + --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ + --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ + --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ + --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ + --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ + --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ + --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ + --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ + --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ + --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ + --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ + --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ + --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ + --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ + --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ + --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ + --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ + --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ + --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ + --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ + --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ + --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ + --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ + --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ + --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ + --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ + --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ + --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ + --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ + --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ + --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ + --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ + --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ + --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ + --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ + --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ + --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ + --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ + --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ + --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ + --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ + --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b + # via cryptography +charset-normalizer==3.4.0 \ + --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ + --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ + --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ + --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ + --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ + --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ + --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ + --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ + --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ + --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ + --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ + --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ + --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ + --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ + --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ + --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ + --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ + --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ + --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ + --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ + --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ + --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ + --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ + --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ + --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ + --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ + --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ + --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ + --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ + --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ + --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ + --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ + --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ + --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ + --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ + --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ + --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ + --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ + --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ + --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ + --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ + --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ + --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ + --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ + --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ + --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ + --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ + --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ + --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ + --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ + --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ + --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ + --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ + --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ + --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ + --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ + --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ + --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ + --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ + --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ + --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ + --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ + --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ + --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ + --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ + --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ + --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ + --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ + --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ + --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ + --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ + --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ + --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ + --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ + --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ + --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ + --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ + --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ + --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ + --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ + --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ + --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ + --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ + --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ + --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ + --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ + --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ + --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ + --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ + --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ + --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ + --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ + --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ + --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ + --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ + --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ + --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ + --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ + --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ + --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ + --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ + --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ + --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ + --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ + --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 + # via requests +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via + # lsst-daf-butler + # lsst-dax-obscore + # lsst-felis + # safir + # uvicorn +colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # click + # uvicorn +cryptography==43.0.1 \ + --hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \ + --hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \ + --hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \ + --hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \ + --hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \ + --hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \ + --hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \ + --hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \ + --hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \ + --hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \ + --hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \ + --hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \ + --hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \ + --hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \ + --hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \ + --hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \ + --hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \ + --hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \ + --hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \ + --hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \ + --hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \ + --hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \ + --hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \ + --hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \ + --hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \ + --hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \ + --hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289 + # via + # pyjwt + # safir +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via + # -r requirements/main.in + # lsst-resources +deprecated==1.2.14 \ + --hash=sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c \ + --hash=sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3 + # via + # lsst-daf-butler + # lsst-daf-relation + # lsst-utils +fastapi==0.115.2 \ + --hash=sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee \ + --hash=sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86 + # via + # -r requirements/main.in + # safir +gidgethub==5.3.0 \ + --hash=sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0 \ + --hash=sha256:9ece7d37fbceb819b80560e7ed58f936e48a65d37ec5f56db79145156b426a25 + # via safir +google-api-core==2.21.0 \ + --hash=sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81 \ + --hash=sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d + # via + # google-cloud-core + # google-cloud-storage +google-auth==2.35.0 \ + --hash=sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f \ + --hash=sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a + # via + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 \ + --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ + --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 + # via google-cloud-storage +google-cloud-storage==2.18.2 \ + --hash=sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166 \ + --hash=sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99 + # via -r requirements/main.in +google-crc32c==1.6.0 \ + --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ + --hash=sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d \ + --hash=sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e \ + --hash=sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57 \ + --hash=sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2 \ + --hash=sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8 \ + --hash=sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc \ + --hash=sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42 \ + --hash=sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f \ + --hash=sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa \ + --hash=sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b \ + --hash=sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc \ + --hash=sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760 \ + --hash=sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d \ + --hash=sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7 \ + --hash=sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d \ + --hash=sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0 \ + --hash=sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3 \ + --hash=sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3 \ + --hash=sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00 \ + --hash=sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871 \ + --hash=sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c \ + --hash=sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9 \ + --hash=sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205 \ + --hash=sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc \ + --hash=sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d \ + --hash=sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.2 \ + --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ + --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 + # via google-cloud-storage +googleapis-common-protos==1.65.0 \ + --hash=sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63 \ + --hash=sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0 + # via google-api-core +greenlet==3.1.1 ; (python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64') \ + --hash=sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e \ + --hash=sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7 \ + --hash=sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01 \ + --hash=sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1 \ + --hash=sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159 \ + --hash=sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563 \ + --hash=sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83 \ + --hash=sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9 \ + --hash=sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395 \ + --hash=sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa \ + --hash=sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942 \ + --hash=sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1 \ + --hash=sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441 \ + --hash=sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22 \ + --hash=sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9 \ + --hash=sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0 \ + --hash=sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba \ + --hash=sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3 \ + --hash=sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1 \ + --hash=sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6 \ + --hash=sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291 \ + --hash=sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39 \ + --hash=sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d \ + --hash=sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467 \ + --hash=sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475 \ + --hash=sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef \ + --hash=sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c \ + --hash=sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511 \ + --hash=sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c \ + --hash=sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822 \ + --hash=sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a \ + --hash=sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8 \ + --hash=sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d \ + --hash=sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01 \ + --hash=sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145 \ + --hash=sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80 \ + --hash=sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13 \ + --hash=sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e \ + --hash=sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b \ + --hash=sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1 \ + --hash=sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef \ + --hash=sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc \ + --hash=sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff \ + --hash=sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120 \ + --hash=sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437 \ + --hash=sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd \ + --hash=sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981 \ + --hash=sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36 \ + --hash=sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a \ + --hash=sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798 \ + --hash=sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7 \ + --hash=sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761 \ + --hash=sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0 \ + --hash=sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e \ + --hash=sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af \ + --hash=sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa \ + --hash=sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c \ + --hash=sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42 \ + --hash=sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e \ + --hash=sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81 \ + --hash=sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e \ + --hash=sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617 \ + --hash=sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc \ + --hash=sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de \ + --hash=sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111 \ + --hash=sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383 \ + --hash=sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70 \ + --hash=sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6 \ + --hash=sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4 \ + --hash=sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011 \ + --hash=sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803 \ + --hash=sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79 \ + --hash=sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f + # via sqlalchemy +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via + # httpcore + # uvicorn +hpgeom==1.4.0 \ + --hash=sha256:1429d101db6342febc40fca5eb5d407603fe2af4cda3b3bf9ddb6173b0c058fd \ + --hash=sha256:2d6dc38747dc5ab07153a235787393df3268160eb9cf5a5940854d9f87a456e1 \ + --hash=sha256:5650f5be65f3aecf9b86fce8fe4b22895e0b3480fb3ecfa2ffc3b7fa1138a4e9 \ + --hash=sha256:56b75fea7ff008e40a1acbe37ab112a6e0d3d3d018a16606ad12b93cd79412c3 \ + --hash=sha256:832a4037399187a74046a7337c00564ab522a432d64dd728c375008bb6230c84 \ + --hash=sha256:8c73de4b08abf736b2dadeca4a6428b514df0c9387bc560e7428ef7a2ae5b723 \ + --hash=sha256:8f3af4974d86e8d922dd5b81badb93e2d9dcc068538bd93eab002a23ebd9f7f2 \ + --hash=sha256:91abaf5841215dc8bcccd194f0d7351f7bd7163ec6ff69449abfb20a76250506 \ + --hash=sha256:95f7b355a4ca9e923e7b37f13f14c828670435645c51fb41e92223894532a26e \ + --hash=sha256:96e051baa06fff9514e2b264649e192f560130142352628bf3ae1fb7d7884848 \ + --hash=sha256:978dd7d2dfcf06b621981fb7f5635267f17336dd5dc8456113ab0dd1f290de78 \ + --hash=sha256:ac52505e5897d1d793019ee03ee673b451479488c5ab59dbcb13946cf1cc26cc \ + --hash=sha256:adc5e9a27c5da2219c1ef76c6ddf2ed4f74ea217e3a639d430e84a567de6ec0c \ + --hash=sha256:c4fa7ba41f6c4802dcfea52f47ce15def620bec6846cbf02f50f7a4745fb66c2 \ + --hash=sha256:df8b0d92b8dcd4f1c64ebeb0e9a85a68579623d2fac96a00fa628223d564a12d \ + --hash=sha256:e19c5068894cc470e2c3766eb6743ea8b583c4338b2eaae1d4ba8a586d414f4a \ + --hash=sha256:e70c81bf01c6b2bbe417a4b164d0effb349259b9ddcaed81a14b02a57a3a13b6 + # via lsst-sphgeom +httpcore==1.0.6 \ + --hash=sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f \ + --hash=sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f + # via httpx +httptools==0.6.4 \ + --hash=sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a \ + --hash=sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd \ + --hash=sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2 \ + --hash=sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17 \ + --hash=sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8 \ + --hash=sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3 \ + --hash=sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5 \ + --hash=sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da \ + --hash=sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0 \ + --hash=sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721 \ + --hash=sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636 \ + --hash=sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff \ + --hash=sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0 \ + --hash=sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071 \ + --hash=sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c \ + --hash=sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4 \ + --hash=sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1 \ + --hash=sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9 \ + --hash=sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44 \ + --hash=sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083 \ + --hash=sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003 \ + --hash=sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959 \ + --hash=sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc \ + --hash=sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076 \ + --hash=sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490 \ + --hash=sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660 \ + --hash=sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6 \ + --hash=sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c \ + --hash=sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50 \ + --hash=sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547 \ + --hash=sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba \ + --hash=sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440 \ + --hash=sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988 \ + --hash=sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab \ + --hash=sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970 \ + --hash=sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1 \ + --hash=sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2 \ + --hash=sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f \ + --hash=sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81 \ + --hash=sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069 \ + --hash=sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975 \ + --hash=sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f \ + --hash=sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43 + # via uvicorn +httpx==0.27.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 + # via + # lsst-daf-butler + # safir +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # anyio + # httpx + # requests +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d + # via -r requirements/main.in +jmespath==1.0.1 \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via + # boto3 + # botocore +lsst-daf-butler==27.2024.4100 \ + --hash=sha256:28f07e3e28c4ab4e5c1b09d7648a405fc2935c31d0698ee8b3a87c2ad50b77e1 \ + --hash=sha256:8db10c0fdaef2b8dd3e012fd0cadc6efe6f58817b6d2b8ed38564e5fe3e2db83 + # via + # -r requirements/main.in + # lsst-dax-obscore +lsst-daf-relation==27.2024.4100 \ + --hash=sha256:b83e67cccd2f063ecbe6e59e7cb8bdebca2279f6b006b0684f0dd4709942e52b \ + --hash=sha256:ca1aed908765d337bdecd3a6fd63d488546fce41b2607ac3eb7efba7624b8389 + # via lsst-daf-butler +lsst-dax-obscore @ git+https://github.com/lsst-dm/dax_obscore@5919c78c29630b1c9ed7544a1e075d8cc7ebda22#egg=lsst-dax-obscore + # via -r requirements/main.in +lsst-felis==27.2024.4100 \ + --hash=sha256:09fa50b45e9d3bf3f5cbb347338aa810c278b815dc8e601a5b3486348d62266d \ + --hash=sha256:384127fa69bc15d3b37694b3b8261c210049f162e2d1e440b204614b046e7191 + # via lsst-dax-obscore +lsst-resources==27.2024.4100 \ + --hash=sha256:7471ca3644f2ad809212d4f897a4c6ab2123ab07f4def2ca8e0b670b63283bcd \ + --hash=sha256:cb141792984ec2c459a7ceed31e1e052fa7a4387a9174665d0306dcc1723eadd + # via + # lsst-daf-butler + # lsst-dax-obscore +lsst-sphgeom==27.2024.3700 \ + --hash=sha256:00299bd4fe9b4c7209e6b70f3d5da93c48e2a59a27fa6cc84929f2ad9905b2f4 \ + --hash=sha256:314734fde962abb04a86e47c30df012356aec0c6a1d8599ae79aba3023a8f222 \ + --hash=sha256:384705377ea7eeb9cca1ceec9e31ae6f61be4d482f5b43760d69b98d55e397ae \ + --hash=sha256:3897fd2cb6043f0ff04ee29954862d0069c5d6e907e26853b3d199671f96088a \ + --hash=sha256:3e0a1cb18289dd66780db8517b57457ed8537e0964465710970bc3ddb6137a4d \ + --hash=sha256:3edbfe4081e37f017c92c8d8c4f2f83004917678a35c0f984a5eb465742d4d6c \ + --hash=sha256:4aa13875db43bc71fe6f272acdc8e52d8101dbf97d04c4272b2b1c0b23d7defc \ + --hash=sha256:4ee44b5d3fc1a62bbfa885a8336bca6592fdfccdec365142d7f889df8a47240a \ + --hash=sha256:558e9bb8c86e44aded38f0d67e46793718c47cde54b61c66b65d4adcf6fc4edd \ + --hash=sha256:55d7590118144acea5fbc6f6aa30bce8aa97480b6dcdd9ce219831f52cd614ad \ + --hash=sha256:59fc8ff9caf1d76ccd2ea827063f0b4744369729cd8ffd0a2b847c6d39e49ff6 \ + --hash=sha256:6789b51db838b3b4e9248b00a51e73b4fea0b697c6833381cd9d17f2409c23ad \ + --hash=sha256:68426af7090c79adf0770a0bf51805de75b3ced25f727929cdb9b292580a13a3 \ + --hash=sha256:735078805433c3ff77deb0458012f20464b63741e42c21f90fd114dec5ba99df \ + --hash=sha256:73b1ded2e5223b9b323d197d8272c3ceaaf7a534f8baed1d8bd2294e06affd8e \ + --hash=sha256:74069b6ebe7e9d0b9c0dd0fa3c570cdc2e573302cd6fb76795fed23350947ee9 \ + --hash=sha256:7592b0dc51299cda568b56b55b126b92963bf0e672137450b1a38a2a227f74c3 \ + --hash=sha256:8262f8b96edefdbe1f9a31c27beb54419fd2b0570952e2ab273337e254a67130 \ + --hash=sha256:853c23c5f13588f602ee5433f6ef9ff9ae1553c07389661c2b4e21a8c0168c03 \ + --hash=sha256:b5b5c74683cafdfe02592790adc76d63250fb122b7d68169bd96dfdfe6baf2d1 \ + --hash=sha256:bc0143ee23c49aeb75339bf10f7f58148148447c1055a16dbbe3022bf08e6222 \ + --hash=sha256:c015ee2d8cd5ff7f2bef8205d2e042b2bd7a944cff12a2cf3c1e64ab0a154cec \ + --hash=sha256:c66c3fa69aee3e2c72dc8a0f4ac48af3b0fcdd08bfa8c89902534e93ff76f2af \ + --hash=sha256:c963b6cff01f44cad5998cbd625b0aeddaa17e36d0e2ea8d77848863a42dd7c0 \ + --hash=sha256:fa712112b2396496c64ef43d6d33b7167d492678345e99dd87cc21c5c226bfb4 + # via + # lsst-daf-butler + # lsst-dax-obscore +lsst-utils==27.2024.4100 \ + --hash=sha256:2ab3ae23920671defdf02899ec1b2cfcfd2629866f34c0bb0fb3588d8e0595a0 \ + --hash=sha256:fbd1a851e6259e58c25fc2171724174aa0bc25738ce0c0e1cc1ca8b0de9c4c25 + # via + # lsst-daf-butler + # lsst-daf-relation + # lsst-dax-obscore + # lsst-felis + # lsst-resources +lxml==5.3.0 \ + --hash=sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e \ + --hash=sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229 \ + --hash=sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3 \ + --hash=sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5 \ + --hash=sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70 \ + --hash=sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15 \ + --hash=sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002 \ + --hash=sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd \ + --hash=sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22 \ + --hash=sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf \ + --hash=sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22 \ + --hash=sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832 \ + --hash=sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727 \ + --hash=sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e \ + --hash=sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30 \ + --hash=sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f \ + --hash=sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f \ + --hash=sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51 \ + --hash=sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4 \ + --hash=sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de \ + --hash=sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875 \ + --hash=sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42 \ + --hash=sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e \ + --hash=sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6 \ + --hash=sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391 \ + --hash=sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc \ + --hash=sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b \ + --hash=sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237 \ + --hash=sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4 \ + --hash=sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86 \ + --hash=sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f \ + --hash=sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a \ + --hash=sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8 \ + --hash=sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f \ + --hash=sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903 \ + --hash=sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03 \ + --hash=sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e \ + --hash=sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99 \ + --hash=sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7 \ + --hash=sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab \ + --hash=sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d \ + --hash=sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22 \ + --hash=sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492 \ + --hash=sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b \ + --hash=sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3 \ + --hash=sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be \ + --hash=sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469 \ + --hash=sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f \ + --hash=sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a \ + --hash=sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c \ + --hash=sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a \ + --hash=sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4 \ + --hash=sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94 \ + --hash=sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442 \ + --hash=sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b \ + --hash=sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84 \ + --hash=sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c \ + --hash=sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9 \ + --hash=sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1 \ + --hash=sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be \ + --hash=sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367 \ + --hash=sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e \ + --hash=sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21 \ + --hash=sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa \ + --hash=sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16 \ + --hash=sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d \ + --hash=sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe \ + --hash=sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83 \ + --hash=sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba \ + --hash=sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040 \ + --hash=sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763 \ + --hash=sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8 \ + --hash=sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff \ + --hash=sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2 \ + --hash=sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a \ + --hash=sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b \ + --hash=sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce \ + --hash=sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c \ + --hash=sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577 \ + --hash=sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8 \ + --hash=sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71 \ + --hash=sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512 \ + --hash=sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540 \ + --hash=sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f \ + --hash=sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2 \ + --hash=sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a \ + --hash=sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce \ + --hash=sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e \ + --hash=sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2 \ + --hash=sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27 \ + --hash=sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1 \ + --hash=sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d \ + --hash=sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1 \ + --hash=sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330 \ + --hash=sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920 \ + --hash=sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99 \ + --hash=sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff \ + --hash=sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18 \ + --hash=sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff \ + --hash=sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c \ + --hash=sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179 \ + --hash=sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080 \ + --hash=sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19 \ + --hash=sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d \ + --hash=sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70 \ + --hash=sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32 \ + --hash=sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a \ + --hash=sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2 \ + --hash=sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79 \ + --hash=sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3 \ + --hash=sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5 \ + --hash=sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f \ + --hash=sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d \ + --hash=sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3 \ + --hash=sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b \ + --hash=sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753 \ + --hash=sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9 \ + --hash=sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957 \ + --hash=sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033 \ + --hash=sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb \ + --hash=sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656 \ + --hash=sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab \ + --hash=sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b \ + --hash=sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d \ + --hash=sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd \ + --hash=sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859 \ + --hash=sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11 \ + --hash=sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c \ + --hash=sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a \ + --hash=sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005 \ + --hash=sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654 \ + --hash=sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80 \ + --hash=sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e \ + --hash=sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec \ + --hash=sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7 \ + --hash=sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965 \ + --hash=sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945 \ + --hash=sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8 + # via pydantic-xml +markupsafe==3.0.1 \ + --hash=sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396 \ + --hash=sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38 \ + --hash=sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a \ + --hash=sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8 \ + --hash=sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b \ + --hash=sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad \ + --hash=sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a \ + --hash=sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a \ + --hash=sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da \ + --hash=sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6 \ + --hash=sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8 \ + --hash=sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344 \ + --hash=sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a \ + --hash=sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8 \ + --hash=sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5 \ + --hash=sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7 \ + --hash=sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170 \ + --hash=sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132 \ + --hash=sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9 \ + --hash=sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd \ + --hash=sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9 \ + --hash=sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346 \ + --hash=sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc \ + --hash=sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589 \ + --hash=sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5 \ + --hash=sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915 \ + --hash=sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295 \ + --hash=sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453 \ + --hash=sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea \ + --hash=sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b \ + --hash=sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d \ + --hash=sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b \ + --hash=sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4 \ + --hash=sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b \ + --hash=sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7 \ + --hash=sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf \ + --hash=sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f \ + --hash=sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91 \ + --hash=sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd \ + --hash=sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50 \ + --hash=sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b \ + --hash=sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583 \ + --hash=sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a \ + --hash=sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984 \ + --hash=sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c \ + --hash=sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c \ + --hash=sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25 \ + --hash=sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa \ + --hash=sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4 \ + --hash=sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3 \ + --hash=sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97 \ + --hash=sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1 \ + --hash=sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd \ + --hash=sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772 \ + --hash=sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a \ + --hash=sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729 \ + --hash=sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca \ + --hash=sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6 \ + --hash=sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635 \ + --hash=sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b \ + --hash=sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f + # via jinja2 +numpy==2.1.2 \ + --hash=sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8 \ + --hash=sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466 \ + --hash=sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35 \ + --hash=sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c \ + --hash=sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4 \ + --hash=sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6 \ + --hash=sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0 \ + --hash=sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7 \ + --hash=sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a \ + --hash=sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a \ + --hash=sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e \ + --hash=sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62 \ + --hash=sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2 \ + --hash=sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5 \ + --hash=sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee \ + --hash=sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe \ + --hash=sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a \ + --hash=sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e \ + --hash=sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf \ + --hash=sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c \ + --hash=sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3 \ + --hash=sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86 \ + --hash=sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df \ + --hash=sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98 \ + --hash=sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d \ + --hash=sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2 \ + --hash=sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146 \ + --hash=sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550 \ + --hash=sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8 \ + --hash=sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb \ + --hash=sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e \ + --hash=sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d \ + --hash=sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366 \ + --hash=sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0 \ + --hash=sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db \ + --hash=sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe \ + --hash=sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426 \ + --hash=sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952 \ + --hash=sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03 \ + --hash=sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f \ + --hash=sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7 \ + --hash=sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b \ + --hash=sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17 \ + --hash=sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5 \ + --hash=sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1 \ + --hash=sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142 \ + --hash=sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884 \ + --hash=sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a \ + --hash=sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9 \ + --hash=sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445 \ + --hash=sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1 \ + --hash=sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1 \ + --hash=sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648 + # via + # -r requirements/main.in + # astropy + # hpgeom + # lsst-daf-butler + # lsst-sphgeom + # lsst-utils + # pyarrow + # pyerfa +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via astropy +proto-plus==1.24.0 \ + --hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \ + --hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12 + # via google-api-core +protobuf==5.28.2 \ + --hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \ + --hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \ + --hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \ + --hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \ + --hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \ + --hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \ + --hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \ + --hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \ + --hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \ + --hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \ + --hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d + # via + # google-api-core + # googleapis-common-protos + # proto-plus +psutil==6.0.0 \ + --hash=sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35 \ + --hash=sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0 \ + --hash=sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c \ + --hash=sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1 \ + --hash=sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3 \ + --hash=sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c \ + --hash=sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd \ + --hash=sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3 \ + --hash=sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0 \ + --hash=sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2 \ + --hash=sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6 \ + --hash=sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d \ + --hash=sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c \ + --hash=sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0 \ + --hash=sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132 \ + --hash=sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14 \ + --hash=sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0 + # via lsst-utils +psycopg2-binary==2.9.10 \ + --hash=sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff \ + --hash=sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5 \ + --hash=sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f \ + --hash=sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5 \ + --hash=sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0 \ + --hash=sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c \ + --hash=sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c \ + --hash=sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341 \ + --hash=sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f \ + --hash=sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7 \ + --hash=sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d \ + --hash=sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007 \ + --hash=sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92 \ + --hash=sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb \ + --hash=sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5 \ + --hash=sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5 \ + --hash=sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8 \ + --hash=sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1 \ + --hash=sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68 \ + --hash=sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73 \ + --hash=sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1 \ + --hash=sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53 \ + --hash=sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d \ + --hash=sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906 \ + --hash=sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0 \ + --hash=sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2 \ + --hash=sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a \ + --hash=sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b \ + --hash=sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44 \ + --hash=sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648 \ + --hash=sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7 \ + --hash=sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f \ + --hash=sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa \ + --hash=sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697 \ + --hash=sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d \ + --hash=sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b \ + --hash=sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526 \ + --hash=sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4 \ + --hash=sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287 \ + --hash=sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e \ + --hash=sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673 \ + --hash=sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0 \ + --hash=sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30 \ + --hash=sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3 \ + --hash=sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e \ + --hash=sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92 \ + --hash=sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a \ + --hash=sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c \ + --hash=sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8 \ + --hash=sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909 \ + --hash=sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47 \ + --hash=sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864 \ + --hash=sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc \ + --hash=sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00 \ + --hash=sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb \ + --hash=sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539 \ + --hash=sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b \ + --hash=sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481 \ + --hash=sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5 \ + --hash=sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4 \ + --hash=sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64 \ + --hash=sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392 \ + --hash=sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4 \ + --hash=sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1 \ + --hash=sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1 \ + --hash=sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567 \ + --hash=sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863 + # via -r requirements/main.in +pyarrow==17.0.0 \ + --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ + --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ + --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ + --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ + --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ + --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ + --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ + --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ + --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ + --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ + --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ + --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ + --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ + --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ + --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ + --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ + --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ + --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ + --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ + --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ + --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ + --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ + --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ + --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ + --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ + --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ + --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ + --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ + --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ + --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ + --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ + --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ + --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ + --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ + --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ + --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 + # via + # lsst-daf-butler + # lsst-dax-obscore +pyasn1==0.6.1 \ + --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ + --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.1 \ + --hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \ + --hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c + # via google-auth +pycparser==2.22 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc + # via cffi +pydantic==2.9.2 \ + --hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \ + --hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12 + # via + # -r requirements/main.in + # fastapi + # lsst-daf-butler + # lsst-daf-relation + # lsst-felis + # pydantic-settings + # pydantic-xml + # safir + # vo-models +pydantic-core==2.23.4 \ + --hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \ + --hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \ + --hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \ + --hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \ + --hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \ + --hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \ + --hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \ + --hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \ + --hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \ + --hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \ + --hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \ + --hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \ + --hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \ + --hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \ + --hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \ + --hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \ + --hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \ + --hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \ + --hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \ + --hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \ + --hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \ + --hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \ + --hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \ + --hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \ + --hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \ + --hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \ + --hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \ + --hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \ + --hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \ + --hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \ + --hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \ + --hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \ + --hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \ + --hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \ + --hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \ + --hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \ + --hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \ + --hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \ + --hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \ + --hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \ + --hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \ + --hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \ + --hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \ + --hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \ + --hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \ + --hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \ + --hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \ + --hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \ + --hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \ + --hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \ + --hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \ + --hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \ + --hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \ + --hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \ + --hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \ + --hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \ + --hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \ + --hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \ + --hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \ + --hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \ + --hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \ + --hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \ + --hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \ + --hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \ + --hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \ + --hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \ + --hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \ + --hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \ + --hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \ + --hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \ + --hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \ + --hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \ + --hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \ + --hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \ + --hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \ + --hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \ + --hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \ + --hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \ + --hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \ + --hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \ + --hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \ + --hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \ + --hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \ + --hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \ + --hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \ + --hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \ + --hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \ + --hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \ + --hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607 + # via + # pydantic + # pydantic-xml + # safir +pydantic-settings==2.5.2 \ + --hash=sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907 \ + --hash=sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0 + # via -r requirements/main.in +pydantic-xml==2.13.1 \ + --hash=sha256:225d96ce8288abf84d34aa5c70cd4a834c389a7efb071f95301cbba41bfbec15 \ + --hash=sha256:f880394e090cef43e55aa848b285ea9807011f768d682188807a741b978d7326 + # via vo-models +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.9.0 \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c + # via gidgethub +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via botocore +python-dotenv==1.0.1 \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a + # via + # pydantic-settings + # uvicorn +python-multipart==0.0.12 \ + --hash=sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb \ + --hash=sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf + # via -r requirements/main.in +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via + # astropy + # lsst-daf-butler + # lsst-dax-obscore + # lsst-felis + # lsst-utils + # uvicorn +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # -r requirements/main.in + # google-api-core + # google-cloud-storage + # lsst-resources +rsa==4.9 \ + --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ + --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 + # via google-auth +s3transfer==0.10.3 \ + --hash=sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d \ + --hash=sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c + # via boto3 +safir==6.4.0 \ + --hash=sha256:ba7af071eab0d198e6e15a2117028566f3f4237e02e2278e8bfc2633a7c68228 \ + --hash=sha256:f38c3f1d7d76d304984b572288826510e5c7a0e1f965b2eabdd7f3bace07c48a + # via -r requirements/main.in +safir-logging==6.4.0 \ + --hash=sha256:4031a430d738b8fe5bfd29125dce6cbf4e4949879307ba4146648afa3d24cd0a \ + --hash=sha256:e2dbf0b5d9dabecd70c27bff9bf01629bf0724b05b0f0087a1fe4f45c702215f + # via safir +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via python-dateutil +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # anyio + # httpx +sqlalchemy==2.0.36 \ + --hash=sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763 \ + --hash=sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436 \ + --hash=sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2 \ + --hash=sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588 \ + --hash=sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e \ + --hash=sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959 \ + --hash=sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d \ + --hash=sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575 \ + --hash=sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908 \ + --hash=sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8 \ + --hash=sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8 \ + --hash=sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545 \ + --hash=sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7 \ + --hash=sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971 \ + --hash=sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855 \ + --hash=sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c \ + --hash=sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71 \ + --hash=sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d \ + --hash=sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb \ + --hash=sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72 \ + --hash=sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f \ + --hash=sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5 \ + --hash=sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346 \ + --hash=sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24 \ + --hash=sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e \ + --hash=sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5 \ + --hash=sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08 \ + --hash=sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793 \ + --hash=sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88 \ + --hash=sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686 \ + --hash=sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b \ + --hash=sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2 \ + --hash=sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28 \ + --hash=sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d \ + --hash=sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5 \ + --hash=sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a \ + --hash=sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a \ + --hash=sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3 \ + --hash=sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf \ + --hash=sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5 \ + --hash=sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef \ + --hash=sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689 \ + --hash=sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c \ + --hash=sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b \ + --hash=sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07 \ + --hash=sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa \ + --hash=sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06 \ + --hash=sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1 \ + --hash=sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff \ + --hash=sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa \ + --hash=sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687 \ + --hash=sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4 \ + --hash=sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb \ + --hash=sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44 \ + --hash=sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c \ + --hash=sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e \ + --hash=sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53 + # via + # lsst-daf-butler + # lsst-daf-relation + # lsst-dax-obscore + # lsst-felis +starlette==0.40.0 \ + --hash=sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35 \ + --hash=sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4 + # via + # -r requirements/main.in + # fastapi + # safir +structlog==24.4.0 \ + --hash=sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610 \ + --hash=sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4 + # via + # safir + # safir-logging +threadpoolctl==3.5.0 \ + --hash=sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107 \ + --hash=sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467 + # via lsst-utils +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # fastapi + # pydantic + # pydantic-core + # sqlalchemy +uritemplate==4.1.1 \ + --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ + --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e + # via gidgethub +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 + # via + # botocore + # lsst-resources + # requests +uvicorn==0.32.0 \ + --hash=sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82 \ + --hash=sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e + # via -r requirements/main.in +uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \ + --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ + --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ + --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ + --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ + --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ + --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ + --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ + --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ + --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ + --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ + --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ + --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ + --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ + --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ + --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ + --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ + --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ + --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ + --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ + --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ + --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ + --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ + --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ + --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ + --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ + --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ + --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ + --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ + --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ + --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ + --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ + --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ + --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ + --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ + --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ + --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ + --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 + # via uvicorn +vo-models==0.4.1 \ + --hash=sha256:088f09dfd7cbeef38202dc7554168fb32398a64b78bb6f72fdec1bcaab603e40 \ + --hash=sha256:bc55d1284f511f4453a9b92cd3322699e53bbf54f26134d31840a51e3e883efe + # via -r requirements/main.in +watchfiles==0.24.0 \ + --hash=sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a \ + --hash=sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22 \ + --hash=sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a \ + --hash=sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0 \ + --hash=sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827 \ + --hash=sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1 \ + --hash=sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c \ + --hash=sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e \ + --hash=sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188 \ + --hash=sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b \ + --hash=sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5 \ + --hash=sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90 \ + --hash=sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef \ + --hash=sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b \ + --hash=sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15 \ + --hash=sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48 \ + --hash=sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e \ + --hash=sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df \ + --hash=sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd \ + --hash=sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91 \ + --hash=sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d \ + --hash=sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e \ + --hash=sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4 \ + --hash=sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a \ + --hash=sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370 \ + --hash=sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1 \ + --hash=sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea \ + --hash=sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04 \ + --hash=sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896 \ + --hash=sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f \ + --hash=sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f \ + --hash=sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43 \ + --hash=sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735 \ + --hash=sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da \ + --hash=sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a \ + --hash=sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61 \ + --hash=sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3 \ + --hash=sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c \ + --hash=sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f \ + --hash=sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361 \ + --hash=sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855 \ + --hash=sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327 \ + --hash=sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5 \ + --hash=sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab \ + --hash=sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633 \ + --hash=sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777 \ + --hash=sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b \ + --hash=sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be \ + --hash=sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f \ + --hash=sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b \ + --hash=sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e \ + --hash=sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b \ + --hash=sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366 \ + --hash=sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823 \ + --hash=sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3 \ + --hash=sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1 \ + --hash=sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f \ + --hash=sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418 \ + --hash=sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886 \ + --hash=sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571 \ + --hash=sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c \ + --hash=sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94 \ + --hash=sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428 \ + --hash=sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234 \ + --hash=sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6 \ + --hash=sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968 \ + --hash=sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9 \ + --hash=sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c \ + --hash=sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e \ + --hash=sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab \ + --hash=sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec \ + --hash=sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444 \ + --hash=sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b \ + --hash=sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c \ + --hash=sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca \ + --hash=sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b \ + --hash=sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18 \ + --hash=sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318 \ + --hash=sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07 \ + --hash=sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430 \ + --hash=sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c \ + --hash=sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83 \ + --hash=sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05 + # via uvicorn +websockets==13.1 \ + --hash=sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a \ + --hash=sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54 \ + --hash=sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23 \ + --hash=sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7 \ + --hash=sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135 \ + --hash=sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700 \ + --hash=sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf \ + --hash=sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5 \ + --hash=sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e \ + --hash=sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c \ + --hash=sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02 \ + --hash=sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a \ + --hash=sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418 \ + --hash=sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f \ + --hash=sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3 \ + --hash=sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68 \ + --hash=sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978 \ + --hash=sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20 \ + --hash=sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295 \ + --hash=sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b \ + --hash=sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6 \ + --hash=sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb \ + --hash=sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a \ + --hash=sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa \ + --hash=sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0 \ + --hash=sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a \ + --hash=sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238 \ + --hash=sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c \ + --hash=sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084 \ + --hash=sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19 \ + --hash=sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d \ + --hash=sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7 \ + --hash=sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9 \ + --hash=sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79 \ + --hash=sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96 \ + --hash=sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6 \ + --hash=sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe \ + --hash=sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842 \ + --hash=sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa \ + --hash=sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3 \ + --hash=sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d \ + --hash=sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51 \ + --hash=sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7 \ + --hash=sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09 \ + --hash=sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096 \ + --hash=sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9 \ + --hash=sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b \ + --hash=sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5 \ + --hash=sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678 \ + --hash=sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea \ + --hash=sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d \ + --hash=sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49 \ + --hash=sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc \ + --hash=sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5 \ + --hash=sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027 \ + --hash=sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0 \ + --hash=sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878 \ + --hash=sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c \ + --hash=sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa \ + --hash=sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f \ + --hash=sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6 \ + --hash=sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2 \ + --hash=sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf \ + --hash=sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708 \ + --hash=sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6 \ + --hash=sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f \ + --hash=sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd \ + --hash=sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2 \ + --hash=sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d \ + --hash=sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7 \ + --hash=sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f \ + --hash=sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5 \ + --hash=sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6 \ + --hash=sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557 \ + --hash=sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14 \ + --hash=sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7 \ + --hash=sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd \ + --hash=sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c \ + --hash=sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17 \ + --hash=sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23 \ + --hash=sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db \ + --hash=sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6 \ + --hash=sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d \ + --hash=sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9 \ + --hash=sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee \ + --hash=sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6 + # via uvicorn +wrapt==1.16.0 \ + --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \ + --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \ + --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \ + --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \ + --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \ + --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \ + --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \ + --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \ + --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \ + --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \ + --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \ + --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \ + --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \ + --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \ + --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \ + --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \ + --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \ + --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \ + --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \ + --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \ + --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \ + --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \ + --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \ + --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \ + --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \ + --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \ + --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \ + --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \ + --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \ + --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \ + --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \ + --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \ + --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \ + --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \ + --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \ + --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \ + --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \ + --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \ + --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \ + --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \ + --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \ + --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \ + --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \ + --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \ + --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \ + --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \ + --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \ + --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \ + --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \ + --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \ + --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \ + --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \ + --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \ + --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \ + --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \ + --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \ + --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \ + --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \ + --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \ + --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \ + --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \ + --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \ + --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \ + --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \ + --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \ + --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \ + --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \ + --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \ + --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \ + --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4 + # via deprecated diff --git a/requirements/tox.txt b/requirements/tox.txt new file mode 100644 index 0000000..08f5d10 --- /dev/null +++ b/requirements/tox.txt @@ -0,0 +1,88 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --output-file requirements/tox.txt requirements/tox.in +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a + # via + # -c requirements/main.txt + # tox +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via tox +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # -c requirements/dev.txt + # -c requirements/main.txt + # tox +distlib==0.3.9 \ + --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ + --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 + # via virtualenv +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 + # via + # tox + # virtualenv +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via + # -c requirements/dev.txt + # -c requirements/main.txt + # pyproject-api + # tox + # tox-uv +platformdirs==4.3.6 \ + --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ + --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb + # via + # tox + # virtualenv +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via + # -c requirements/dev.txt + # tox +pyproject-api==1.8.0 \ + --hash=sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228 \ + --hash=sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496 + # via tox +tox==4.22.0 \ + --hash=sha256:03734d9a9ac138cd1a898a372fb1b8079e2728618ae06dc37cbf3686cfb56eea \ + --hash=sha256:acc6c627cb3316585238d55d2b633e132fea1bdb01b9d93b56bce7caea6ae73d + # via + # -r requirements/tox.in + # tox-uv +tox-uv==1.15.0 \ + --hash=sha256:a5f08c80a3eabc47881e378700e5044b67ba94b03181ae38246627127f6a9183 \ + --hash=sha256:dfe7e48274248458349b47780da7db815c0156bd3751b6486152bbf01d7672fb + # via -r requirements/tox.in +uv==0.4.22 \ + --hash=sha256:062a57ac3aab9a7d41e1b6a66948d563bf47478c719894661ea2c5ed6485a146 \ + --hash=sha256:0904c141f9fd7088d7837fb7ac5e43191236ed9cf8edf824ed838bdc77da7406 \ + --hash=sha256:0ff4ff91a25ed633f4d2556777e1b317262c01f71e8f72dfbc540e97e7eb5392 \ + --hash=sha256:455538b910db65f20a70cf806c5e65cc1d80ea7f40a116ba1c3d4bd1dab933d9 \ + --hash=sha256:48232daa35ebd3e963eea236cf33915a8b0c8a3673d5da35d764f8b1fec0b1b2 \ + --hash=sha256:52605e291f7ab1daca682b7a92b926c2f70e1fc86caaa37cbd56b64587730ea2 \ + --hash=sha256:527d785dafa5bf8fa4aba42188787a4b25c11d005a5f4bd8afda6e8c2c231e1b \ + --hash=sha256:63156e306f860d9fa2bb1d7c9af30053b88276004b2790cd9bbf20cc83ce988b \ + --hash=sha256:7041bf9d2d5d391cebca7778207eb88a96537ff2e93df2ff9f41d6c4057252c3 \ + --hash=sha256:71f3faaa94f60d362a6984fdf7675d6d2d244139de91a7d46e2367caf950951e \ + --hash=sha256:765dac79e5c8e2924efbd4663d4e03f5d7689f1baa98223b298fe4292610a25a \ + --hash=sha256:7be7adf47158c456031b2b78742a432260b5c22e9a86784fa57e7a208b0c3206 \ + --hash=sha256:956c4f0a9eddb8e18003bc39d114c78f6d6b4ba2683a262af043770abee44f2e \ + --hash=sha256:9cf96ddcb6ea2743e4c44fa22b08a4f2fd09cc9c5e228e8ab04b0cd08371c868 \ + --hash=sha256:af70ea49389397d0f6ff43827f73e0e71db0fc45cdf50c7dcff8318d726c8224 \ + --hash=sha256:c96eb12d1bdb1a826cba3c38273604629ac51e723d705aed17ae282650d030f0 \ + --hash=sha256:d9a242b3360c3a62e248053b3a6f618dc59cb5c56f4e30748433a19a002e4bf5 \ + --hash=sha256:e18c42cc99bc2a3f91d43aeb2df61a6d259114fca50dd3818879e9ee12064f7f + # via tox-uv +virtualenv==20.26.6 \ + --hash=sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48 \ + --hash=sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2 + # via tox diff --git a/ruff-shared.toml b/ruff-shared.toml index 0702eaf..abbdbd9 100644 --- a/ruff-shared.toml +++ b/ruff-shared.toml @@ -125,6 +125,7 @@ select = ["ALL"] builtins-ignorelist = [ "all", "any", + "format", "help", "id", "list", diff --git a/src/vosiav2/__init__.py b/src/sia/__init__.py similarity index 80% rename from src/vosiav2/__init__.py rename to src/sia/__init__.py index 12627dd..928da73 100644 --- a/src/vosiav2/__init__.py +++ b/src/sia/__init__.py @@ -1,4 +1,4 @@ -"""The vo-siav2 service.""" +"""The SIA service.""" __all__ = ["__version__"] @@ -8,7 +8,7 @@ """The application version string (PEP 440 / SemVer compatible).""" try: - __version__ = version("vo-siav2") + __version__ = version("sia") except PackageNotFoundError: # package is not installed __version__ = "0.0.0" diff --git a/src/sia/config.py b/src/sia/config.py new file mode 100644 index 0000000..87a5a1f --- /dev/null +++ b/src/sia/config.py @@ -0,0 +1,64 @@ +"""Configuration definition.""" + +from __future__ import annotations + +from typing import Annotated, Self + +from pydantic import Field, HttpUrl, model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict +from safir.logging import LogLevel, Profile + +from .models.data_collections import ButlerDataCollection + +__all__ = ["Config", "config"] + + +class Config(BaseSettings): + """Configuration for sia.""" + + name: str = Field("sia", title="Name of application") + """Name of application.""" + + path_prefix: str = Field("/api/sia", title="URL prefix for application") + """URL prefix for application.""" + + profile: Profile = Field( + Profile.development, title="Application logging profile" + ) + """Application logging profile.""" + + log_level: LogLevel = Field( + LogLevel.INFO, title="Log level of the application's logger" + ) + """Log level of the application's logger.""" + + model_config = SettingsConfigDict(env_prefix="SIA_", case_sensitive=False) + """Configuration for the model settings.""" + + butler_data_collections: Annotated[ + list[ButlerDataCollection], + Field(title="Data collections"), + ] + """Configuration for the data collections.""" + + slack_webhook: Annotated[ + HttpUrl | None, Field(title="Slack webhook for exception reporting") + ] = None + """Slack webhook for exception reporting.""" + + @model_validator(mode="after") + def _validate_butler_data_collections(self) -> Self: + """Validate the Butler data collections.""" + from .exceptions import FatalFaultError + + if len(self.butler_data_collections) == 0: + raise FatalFaultError( + detail="No Data Collections configured. Please configure " + "at least one Data collection." + ) + + return self + + +config = Config() +"""Configuration instance for sia.""" diff --git a/src/sia/constants.py b/src/sia/constants.py new file mode 100644 index 0000000..b211204 --- /dev/null +++ b/src/sia/constants.py @@ -0,0 +1,15 @@ +"""Constants for the SIA service.""" + +__all__ = ["RESPONSEFORMATS", "RESULT_NAME", "SINGLE_PARAMS"] + +RESPONSEFORMATS = {"votable", "application/x-votable"} +"""List of supported response formats for the SIA service.""" + +RESULT_NAME = "result" +"""The name of the result file.""" + +SINGLE_PARAMS = { + "maxrec", + "responseformat", +} +"""Parameters that should be treated as single values.""" diff --git a/src/vosiav2/handlers/__init__.py b/src/sia/dependencies/__init__.py similarity index 100% rename from src/vosiav2/handlers/__init__.py rename to src/sia/dependencies/__init__.py diff --git a/src/sia/dependencies/context.py b/src/sia/dependencies/context.py new file mode 100644 index 0000000..c10ca42 --- /dev/null +++ b/src/sia/dependencies/context.py @@ -0,0 +1,120 @@ +"""Request context dependency for FastAPI. + +This dependency gathers a variety of information into a single object for the +convenience of writing request handlers. It also provides a place to store a +`structlog.BoundLogger` that can gather additional context during processing, +including from dependencies. +""" + +from dataclasses import dataclass +from typing import Annotated, Any + +from fastapi import Depends, Request +from safir.dependencies.logger import logger_dependency +from structlog.stdlib import BoundLogger + +from ..config import Config +from ..factory import Factory +from .labeled_butler_factory import labeled_butler_factory_dependency + +__all__ = [ + "ContextDependency", + "RequestContext", + "context_dependency", +] + + +@dataclass(slots=True) +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 repeated 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.""" + + config: Config + """SIA's configuration.""" + + logger: BoundLogger + """The request logger, rebound with discovered context.""" + + factory: Factory + """The component factory.""" + + def rebind_logger(self, **values: Any) -> None: + """Add the given values to the logging context. + + Parameters + ---------- + **values + Additional values that should be added to the logging context. + """ + self.logger = self.logger.bind(**values) + self.factory.set_logger(self.logger) + + +class ContextDependency: + """Provide a per-request context as a FastAPI dependency. + + Each request gets a `RequestContext`. To save overhead, the portions of + the context that are shared by all requests are collected into the single + process-global `~sia.factory.ProcessContext` and reused with each + request. + """ + + def __init__(self) -> None: + self._config: Config | None = None + + async def __call__( + self, + *, + request: Request, + logger: Annotated[BoundLogger, Depends(logger_dependency)], + ) -> RequestContext: + """Create a per-request context and return it.""" + if not self._config: + raise RuntimeError("ContextDependency not initialized") + + return RequestContext( + request=request, + config=self._config, + logger=logger, + factory=await self.create_factory(logger=logger), + ) + + async def create_factory(self, logger: BoundLogger) -> Factory: + """Create a factory for use outside a request context.""" + if not self._config: + raise RuntimeError("ContextDependency not initialized") + + return Factory( + logger=logger, + config=self._config, + labeled_butler_factory=await labeled_butler_factory_dependency(), + ) + + async def aclose(self) -> None: + """Clean up the per-process configuration.""" + self._config = None + + async def initialize( + self, + config: Config, + ) -> None: + """Initialize the process-wide shared context. + + Parameters + ---------- + config + SIA configuration. + """ + self._config = config + + +context_dependency = ContextDependency() +"""The dependency that will return the per-request context.""" diff --git a/src/sia/dependencies/data_collections.py b/src/sia/dependencies/data_collections.py new file mode 100644 index 0000000..b795061 --- /dev/null +++ b/src/sia/dependencies/data_collections.py @@ -0,0 +1,44 @@ +"""Data collection dependencies.""" + +from typing import Annotated + +from fastapi import Depends, HTTPException + +from ..dependencies.context import RequestContext, context_dependency +from ..models.data_collections import ButlerDataCollection + + +def validate_collection( + collection_name: str, + context: Annotated[RequestContext, Depends(context_dependency)], +) -> ButlerDataCollection: + """Validate the collection name and return the Butler data collection. + + Parameters + ---------- + collection_name + The name of the collection. + context + The request context. + + Returns + ------- + ButlerDataCollection + The Butler data collection. + + Raises + ------ + HTTPException + If the collection is not found. + """ + try: + data_collection_service = ( + context.factory.create_data_collection_service() + ) + return data_collection_service.get_data_collection_by_name( + name=collection_name + ) + except KeyError as exc: + raise HTTPException( + status_code=404, detail=f"Collection '{collection_name}' not found" + ) from exc diff --git a/src/sia/dependencies/labeled_butler_factory.py b/src/sia/dependencies/labeled_butler_factory.py new file mode 100644 index 0000000..8b8c538 --- /dev/null +++ b/src/sia/dependencies/labeled_butler_factory.py @@ -0,0 +1,44 @@ +"""Dependency class for creating a LabeledButlerFactory singleton.""" + +from lsst.daf.butler import LabeledButlerFactory + +from ..config import Config +from ..services.data_collections import DataCollectionService + + +class LabeledButlerFactoryDependency: + """Provides a remote butler factory as a dependency.""" + + def __init__(self) -> None: + self._labeled_butler_factory: LabeledButlerFactory | None = None + + async def initialize( + self, + config: Config, + ) -> None: + """Initialize the dependency.""" + # Get the data repositories from the config in a format suitable for + # the LabeledButlerFactory. + data_repositories = DataCollectionService( + config=config + ).get_data_repositories() + + self._labeled_butler_factory = LabeledButlerFactory( + repositories=data_repositories + ) + + async def __call__(self) -> LabeledButlerFactory: + """Return the LabeledButlerFactory instance.""" + if self._labeled_butler_factory is None: + raise RuntimeError( + "LabeledButlerFactoryDependency is not initialized" + ) + return self._labeled_butler_factory + + async def aclose(self) -> None: + """Close in this case has no effect.""" + self._labeled_butler_factory = None + + +labeled_butler_factory_dependency = LabeledButlerFactoryDependency() +"""The dependency that will return the LabeledButlerFactoryDependency.""" diff --git a/src/sia/dependencies/query_params.py b/src/sia/dependencies/query_params.py new file mode 100644 index 0000000..8bdfe75 --- /dev/null +++ b/src/sia/dependencies/query_params.py @@ -0,0 +1,34 @@ +"""Provides functions to get instances of params.""" + +from collections import defaultdict +from typing import Annotated + +from fastapi import Depends, Request +from lsst.dax.obscore.siav2 import SIAv2Parameters + +from ..constants import SINGLE_PARAMS +from ..models.sia_query_params import SIAQueryParams + + +async def get_sia_params_dependency( + *, + params: Annotated[SIAQueryParams, Depends(SIAQueryParams)], + request: Request, +) -> SIAv2Parameters: + """Parse GET and POST parameters into SIAv2Parameters for SIA query.""" + # For POST requests, use form data + if request.method == "POST": + post_params_ddict: dict[str, list[str]] = defaultdict(list) + + for key, value in (await request.form()).multi_items(): + if not isinstance(value, str): + raise TypeError("File upload not supported") + post_params_ddict[key].append(value) + + post_params = { + key: (values[0] if key in SINGLE_PARAMS and values else values) + for key, values in post_params_ddict.items() + } + params = SIAQueryParams.from_dict(post_params) + + return params.to_butler_parameters() diff --git a/src/sia/dependencies/token.py b/src/sia/dependencies/token.py new file mode 100644 index 0000000..e88320b --- /dev/null +++ b/src/sia/dependencies/token.py @@ -0,0 +1,30 @@ +"""FastAPI dependencies for handling tokens.""" + +from fastapi import Header + + +async def optional_auth_delegated_token_dependency( + x_auth_request_token: str | None = Header( + default=None, include_in_schema=False + ), +) -> str | None: + """Make auth_delegated_token_dependency optional. + The use-case for this is for a Direct Butler query where we don't need to + delegate the token. + + Parameters + ---------- + x_auth_request_token + The delegated token. + + Returns + ------- + Optional[str] + The delegated token or None if it is not provided. + """ + if x_auth_request_token is None: + return None + + from safir.dependencies.gafaelfawr import auth_delegated_token_dependency + + return await auth_delegated_token_dependency(x_auth_request_token) diff --git a/src/sia/errors.py b/src/sia/errors.py new file mode 100644 index 0000000..9d1e746 --- /dev/null +++ b/src/sia/errors.py @@ -0,0 +1,97 @@ +"""VOTable exception handler to format an error into a valid +VOTAble. +""" + +import functools +from collections.abc import Callable +from pathlib import Path +from typing import ParamSpec, TypeVar + +import structlog +from fastapi import Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.templating import Jinja2Templates + +from .config import config +from .exceptions import DefaultFaultError, UsageFaultError, VOTableError + +_TEMPLATES = Jinja2Templates( + directory=str(Path(__file__).resolve().parent / "templates") +) + +logger = structlog.get_logger(config.name) + + +async def votable_exception_handler( + request: Request, exc: Exception +) -> Response: + """Handle exceptions that should be returned as VOTable errors. + Produces a VOTable error as a TemplateResponse with the error message. + + Parameters + ---------- + request + The incoming request. + exc + The exception to handle. + + Returns + ------- + Response + The VOTAble error response. + """ + logger.error( + "Error during query processing", + error_type=type(exc).__name__, + error_message=str(exc), + path=request.url.path, + method=request.method, + ) + + if isinstance(exc, RequestValidationError): + error_message = str(exc) + exc = UsageFaultError(detail=error_message) + elif not isinstance(exc, VOTableError): + exc = DefaultFaultError(detail=str(exc)) + + response = _TEMPLATES.TemplateResponse( + request, + "votable_error.xml", + { + "request": request, + "error_message": str(exc), + }, + media_type="application/xml", + ) + response.status_code = 400 + return response + + +R = TypeVar("R") # Return type +P = ParamSpec("P") # Parameters + + +def handle_exceptions(func: Callable[P, R]) -> Callable[P, R]: + """Handle exceptions in the decorated function by logging + and then formatting as a VOTable. + + Parameters + ---------- + func + The function to decorate. + """ + + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + """Wrap function to handle its exceptions.""" + try: + return func(*args, **kwargs) + except Exception as exc: + logger.exception("An exception occurred during query processing") + if isinstance(exc, VOTableError): + raise exc from exc + if isinstance(exc, RequestValidationError): + raise UsageFaultError(detail=str(exc)) from exc + raise exc from exc + + return wrapper diff --git a/src/sia/exceptions.py b/src/sia/exceptions.py new file mode 100644 index 0000000..a335a39 --- /dev/null +++ b/src/sia/exceptions.py @@ -0,0 +1,107 @@ +"""VOTable exceptions and exception handler to format an error into a valid +VOTAble. +""" + +from pathlib import Path + +from fastapi.exceptions import HTTPException +from fastapi.templating import Jinja2Templates + +_TEMPLATES = Jinja2Templates( + directory=str(Path(__file__).resolve().parent / "templates") +) + + +# Module may be slightly too long, in the future we may want to break it up + + +class VOTableError(HTTPException): + """Exception for VOTable errors.""" + + def __init__( + self, detail: str = "Uknown error occured", status_code: int = 400 + ) -> None: + super().__init__(detail=detail, status_code=status_code) + + def __str__(self) -> str: + return f"{self.detail}" + + +class UsageFaultError(VOTableError): + """Exception for invalid input. + + Attributes + ---------- + detail + The error message. + status_code + The status code for the exception + """ + + def __init__( + self, detail: str = "Invalid input", status_code: int = 400 + ) -> None: + self.detail = f"UsageFault: {detail}" + self.status_code = status_code + super().__init__(detail=self.detail, status_code=self.status_code) + + +class TransientFaultError(VOTableError): + """Exception for service temporarily unavailable. + + Attributes + ---------- + detail + The error message. + status_code + The status code for the exception + """ + + def __init__( + self, + detail: str = "Service is not currently able to function", + status_code: int = 400, + ) -> None: + self.detail = f"TransientFault: {detail}" + self.status_code = status_code + super().__init__(detail=self.detail, status_code=self.status_code) + + +class FatalFaultError(VOTableError): + """Exception for service cannot perform requested action. + + Attributes + ---------- + detail + The error message. + status_code + The status code for the exception + """ + + def __init__( + self, + detail: str = "Service cannot perform requested action", + status_code: int = 400, + ) -> None: + self.detail = f"FatalFault: {detail}" + self.status_code = status_code + super().__init__(detail=self.detail, status_code=self.status_code) + + +class DefaultFaultError(VOTableError): + """General exception for errors not covered above. + + Attributes + ---------- + detail + The error message. + status_code + The status code for the exception + """ + + def __init__( + self, detail: str = "General error", status_code: int = 400 + ) -> None: + self.detail = f"DefaultFault: {detail}" + self.status_code = status_code + super().__init__(detail=self.detail, status_code=self.status_code) diff --git a/src/sia/factory.py b/src/sia/factory.py new file mode 100644 index 0000000..8806100 --- /dev/null +++ b/src/sia/factory.py @@ -0,0 +1,97 @@ +"""Component factory and process-wide status for mobu.""" + +from __future__ import annotations + +import structlog +from lsst.daf.butler import Butler, LabeledButlerFactory +from lsst.daf.butler.registry import RegistryDefaults +from structlog.stdlib import BoundLogger + +from .config import Config +from .models.data_collections import ButlerDataCollection +from .services.data_collections import DataCollectionService + +__all__ = ["Factory"] + + +class Factory: + """Component factory for sia. + + Uses the contents of a `ProcessContext` to construct the components of an + application on demand. + + Parameters + ---------- + config + The configuration instance + labeled_butler_factory + The LabeledButlerFactory singleton + logger + The logger instance + """ + + def __init__( + self, + config: Config, + labeled_butler_factory: LabeledButlerFactory, + logger: BoundLogger | None = None, + ) -> None: + self._config = config + self._labeled_butler_factory = labeled_butler_factory + self._logger = ( + logger if logger else structlog.get_logger(self._config.name) + ) + + def create_butler( + self, + butler_collection: ButlerDataCollection, + token: str | None = None, + ) -> Butler: + """Create a Butler instance. + + Parameters + ---------- + butler_collection + The Butler data collection. + token + The token to use for the Butler instance. + + Returns + ------- + Butler + The Butler instance. + """ + butler = self._labeled_butler_factory.create_butler( + label=butler_collection.label, access_token=token + ) + + # Temporary workaround + butler.registry.defaults = RegistryDefaults( + instrument=butler_collection.default_instrument, + ) + return butler + + def create_data_collection_service(self) -> DataCollectionService: + """Create a data collection service. + + Returns + ------- + DataCollectionService + The data collection service. + """ + return DataCollectionService( + config=self._config, + ) + + def set_logger(self, logger: BoundLogger) -> None: + """Replace the internal logger. + + Used by the context dependency to update the logger for all + newly-created components when it's rebound with additional context. + + Parameters + ---------- + logger + New logger. + """ + self._logger = logger diff --git a/src/sia/handlers/__init__.py b/src/sia/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sia/handlers/external.py b/src/sia/handlers/external.py new file mode 100644 index 0000000..6e34111 --- /dev/null +++ b/src/sia/handlers/external.py @@ -0,0 +1,197 @@ +"""Handlers for the app's external root, ``/api/sia/``.""" + +from pathlib import Path +from typing import Annotated + +from fastapi import APIRouter, Depends, Request, Response +from fastapi.templating import Jinja2Templates +from lsst.dax.obscore.siav2 import SIAv2Parameters, siav2_query +from safir.dependencies.logger import logger_dependency +from safir.metadata import get_metadata +from safir.models import ErrorModel +from structlog.stdlib import BoundLogger +from vo_models.vosi.availability import Availability +from vo_models.vosi.capabilities.models import VOSICapabilities + +from ..config import config +from ..dependencies.context import RequestContext, context_dependency +from ..dependencies.data_collections import validate_collection +from ..dependencies.query_params import get_sia_params_dependency +from ..dependencies.token import optional_auth_delegated_token_dependency +from ..models.data_collections import ButlerDataCollection +from ..models.index import Index +from ..services.availability import AvailabilityService +from ..services.data_collections import DataCollectionService +from ..services.response_handler import ResponseHandlerService + +BASE_DIR = Path(__file__).resolve().parent.parent +_TEMPLATES = Jinja2Templates(directory=str(Path(BASE_DIR, "templates"))) + +__all__ = ["get_index", "external_router"] + +external_router = APIRouter() +"""FastAPI router for all external handlers.""" + + +@external_router.get( + "/", + description=( + "Document the top-level API here. By default it only returns metadata" + " about the application." + ), + response_model=Index, + response_model_exclude_none=True, + summary="Application metadata", +) +async def get_index( + logger: Annotated[BoundLogger, Depends(logger_dependency)], +) -> Index: + """GET ``/api/sia/`` (the app's external root). + + Customize this handler to return whatever the top-level resource of your + application should return. For example, consider listing key API URLs. + When doing so, also change or customize the response model in + `sia.models.Index`. + + By convention, the root of the external API includes a field called + ``metadata`` that provides the same Safir-generated metadata as the + internal root endpoint. + """ + # There is no need to log simple requests since uvicorn will do this + # automatically, but this is included as an example of how to use the + # logger for more complex logging. + logger.info("Request for application metadata") + + metadata = get_metadata( + package_name="sia", + application_name=config.name, + ) + return Index(metadata=metadata) + + +@external_router.get( + "/{collection_name}/availability", + response_model=Availability, + description="VOSI-availability resource for the service", + responses={ + 200: { + "description": "Successful Response", + "content": { + "application/xml": { + "example": """ + + true +""", + "schema": Availability.model_json_schema(), + }, + "application/json": None, + }, + } + }, + summary="IVOA service availability", +) +async def get_availability(collection_name: str) -> Response: + # Get the butler data collection + collection = DataCollectionService( + config=config + ).get_data_collection_by_name(name=collection_name) + + # Check if it is available + availability = await AvailabilityService( + collection=collection + ).get_availability() + + xml = availability.to_xml(skip_empty=True) + return Response(content=xml, media_type="application/xml") + + +@external_router.get( + "/{collection_name}/capabilities", + description="VOSI-capabilities resource for the SIA service.", + response_model=VOSICapabilities, + responses={ + 200: { + "content": { + "application/xml": { + "example": """ + + + + https://example.com/query + + + """, + "schema": VOSICapabilities.model_json_schema(), + } + } + } + }, + summary="IVOA service capabilities", +) +async def get_capabilities( + collection_name: str, + request: Request, +) -> Response: + return _TEMPLATES.TemplateResponse( + request, + "capabilities.xml", + { + "request": request, + "availability_url": request.url_for( + "get_availability", collection_name=collection_name + ), + "capabilities_url": request.url_for( + "get_capabilities", collection_name=collection_name + ), + "query_url": request.url_for( + "query", collection_name=collection_name + ), + }, + media_type="application/xml", + ) + + +@external_router.get( + "/{collection_name}/query", + description="Query endpoint for the SIA service.", + responses={ + 200: {"content": {"application/xml": {}}}, + 400: { + "description": "Invalid query parameters", + "model": ErrorModel, + }, + }, + summary="IVOA SIA service query", +) +@external_router.post( + "/{collection_name}/query", + description="Query endpoint for the SIA service (POST method).", + responses={ + 200: {"content": {"application/xml": {}}}, + 400: { + "description": "Invalid query parameters", + "model": ErrorModel, + }, + }, + summary="IVOA SIA (v2) service query (POST)", +) +async def query( + *, + context: Annotated[RequestContext, Depends(context_dependency)], + collection: Annotated[ButlerDataCollection, Depends(validate_collection)], + params: Annotated[SIAv2Parameters, Depends(get_sia_params_dependency)], + delegated_token: Annotated[ + str | None, Depends(optional_auth_delegated_token_dependency) + ], +) -> Response: + return ResponseHandlerService.process_query( + factory=context.factory, + params=params, + token=delegated_token, + sia_query=siav2_query, + collection=collection, + request=context.request, + ) diff --git a/src/vosiav2/handlers/internal.py b/src/sia/handlers/internal.py similarity index 93% rename from src/vosiav2/handlers/internal.py rename to src/sia/handlers/internal.py index b031337..b5fb4d7 100644 --- a/src/vosiav2/handlers/internal.py +++ b/src/sia/handlers/internal.py @@ -1,7 +1,7 @@ """Internal HTTP handlers that serve relative to the root path, ``/``. These handlers aren't externally visible since the app is available at a path, -``/vo-siav2``. See `vosiav2.handlers.external` for +``sia``. See `sia.handlers.external` for the external endpoint handlers. These handlers should be used for monitoring, health checks, internal status, @@ -37,6 +37,6 @@ async def get_index() -> Metadata: By convention, this endpoint returns only the application's metadata. """ return get_metadata( - package_name="vo-siav2", + package_name="sia", application_name=config.name, ) diff --git a/src/sia/main.py b/src/sia/main.py new file mode 100644 index 0000000..e67ce3c --- /dev/null +++ b/src/sia/main.py @@ -0,0 +1,133 @@ +"""The main application factory for the sia service. + +Notes +----- +Be aware that, following the normal pattern for FastAPI services, the app is +constructed when this module is loaded and is not deferred until a function is +called. +""" + +import json +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from importlib.metadata import metadata, version + +from fastapi import FastAPI, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.openapi.utils import get_openapi +from safir.dependencies.http_client import http_client_dependency +from safir.logging import configure_logging, configure_uvicorn_logging +from safir.middleware.ivoa import CaseInsensitiveQueryMiddleware +from safir.middleware.x_forwarded import XForwardedMiddleware +from safir.slack.webhook import SlackRouteErrorHandler +from structlog import get_logger + +from .config import config +from .dependencies.context import context_dependency +from .dependencies.labeled_butler_factory import ( + labeled_butler_factory_dependency, +) +from .errors import votable_exception_handler +from .exceptions import VOTableError +from .handlers.external import external_router +from .handlers.internal import internal_router +from .middleware.ivoa import CaseInsensitiveFormMiddleware + +__all__ = ["app"] + + +logger = get_logger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """Set up and tear down the application.""" + logger.debug("SIA has started up.") + await labeled_butler_factory_dependency.initialize(config=config) + await context_dependency.initialize(config=config) + + yield + + await labeled_butler_factory_dependency.aclose() + await context_dependency.aclose() + logger.debug("SIA shut down complete.") + await http_client_dependency.aclose() + + +configure_logging( + profile=config.profile, + log_level=config.log_level, + name="sia", +) +configure_uvicorn_logging(config.log_level) + +app = FastAPI( + title="sia", + description=metadata("sia")["Summary"], + version=version("sia"), + openapi_url=f"{config.path_prefix}/openapi.json", + docs_url=f"{config.path_prefix}/docs", + redoc_url=f"{config.path_prefix}/redoc", + lifespan=lifespan, +) +"""The main FastAPI application for sia.""" + + +def configure_exception_handlers(app: FastAPI) -> None: + """Configure the exception handlers for the application. + Handle by formatting as VOTable with the appropriate error message. + + Parameters + ---------- + app + The FastAPI application instance. + """ + + @app.exception_handler(VOTableError) + @app.exception_handler(RequestValidationError) + async def custom_exception_handler( + request: Request, exc: Exception + ) -> Response: + """Handle exceptions that should be returned as VOTable errors. + + Parameters + ---------- + request + The incoming request. + exc + The exception to handle. + """ + return await votable_exception_handler(request, exc) + + +# Address case-sensitivity issue with IVOA query parameters +app.add_middleware(CaseInsensitiveFormMiddleware) +app.add_middleware(CaseInsensitiveQueryMiddleware) + +# Configure exception handlers. +configure_exception_handlers(app) + +# Attach the routers. +app.include_router(internal_router) +app.include_router(external_router, prefix=f"{config.path_prefix}") + +# Add middleware. +app.add_middleware(XForwardedMiddleware) + + +# Configure Slack alerts. +if config.slack_webhook: + webhook = str(config.slack_webhook) + SlackRouteErrorHandler.initialize(webhook, config.name, logger) + logger.debug("Initialized Slack webhook") + + +def create_openapi() -> str: + """Create the OpenAPI spec for static documentation.""" + spec = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + return json.dumps(spec) diff --git a/src/sia/middleware/__init__.py b/src/sia/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sia/middleware/ivoa.py b/src/sia/middleware/ivoa.py new file mode 100644 index 0000000..e08f29b --- /dev/null +++ b/src/sia/middleware/ivoa.py @@ -0,0 +1,134 @@ +"""Middleware for IVOA services.""" + +from copy import copy +from urllib.parse import parse_qsl, urlencode + +from starlette.types import ASGIApp, Receive, Scope, Send + +__all__ = ["CaseInsensitiveFormMiddleware"] + + +class CaseInsensitiveFormMiddleware: + """Make POST parameter keys all lowercase. + + This middleware attempts to work around case-sensitivity issues by + lowercasing POST parameter keys before the request is processed. This + allows normal FastAPI parsing to work without regard for case, permitting + FastAPI to perform input validation on the POST parameters. + """ + + def __init__(self, *, app: ASGIApp) -> None: + """Initialize the middleware with the ASGI application. + + Parameters + ---------- + app + The ASGI application to wrap. + """ + self._app = app + + async def __call__( + self, scope: Scope, receive: Receive, send: Send + ) -> None: + """Process request set query parameters and POST body keys to + lowercase. + """ + if scope["type"] != "http": + await self._app(scope, receive, send) + return + + scope = copy(scope) + + if scope["method"] == "POST" and self.is_form_data(scope): + receive = self.wrapped_receive(receive) + + await self._app(scope, receive, send) + + @staticmethod + def is_form_data(scope: Scope) -> bool: + """Check if the request contains form data. + + Parameters + ---------- + scope + The request scope. + + Returns + ------- + bool + True if the request contains form data, False otherwise. + """ + headers = { + k.decode("latin-1"): v.decode("latin-1") + for k, v in scope.get("headers", []) + } + content_type = headers.get("content-type", "") + return content_type.startswith("application/x-www-form-urlencoded") + + @staticmethod + async def get_body(receive: Receive) -> bytes: + """Read the entire request body. + + Parameters + ---------- + receive + The receive function to read messages from. + + Returns + ------- + bytes + The entire request body. + """ + body = b"" + more_body = True + while more_body: + message = await receive() + body += message.get("body", b"") + more_body = message.get("more_body", False) + return body + + @staticmethod + async def process_form_data(body: bytes) -> bytes: + """Process the body, lowercasing keys of form data. + + Parameters + ---------- + body + The request body. + + Returns + ------- + bytes + The processed request body with lowercased keys. + """ + body_str = body.decode("utf-8") + parsed = parse_qsl(body_str) + lowercased = [(key.lower(), value) for key, value in parsed] + processed = urlencode(lowercased) + return processed.encode("utf-8") + + def wrapped_receive(self, receive: Receive) -> Receive: + """Wrap the receive function to process form data. + + Parameters + ---------- + receive + The receive function to wrap. + + Returns + ------- + Receive + The wrapped receive function. + """ + + async def inner() -> dict: + """Process the form data and return the request.""" + body = await self.get_body(receive) + processed_body = await self.process_form_data(body) + return { + "type": "http.request", + "body": processed_body, + "more_body": False, + } + + return inner diff --git a/src/sia/models/__init__.py b/src/sia/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sia/models/butler_type.py b/src/sia/models/butler_type.py new file mode 100644 index 0000000..a6bfc2e --- /dev/null +++ b/src/sia/models/butler_type.py @@ -0,0 +1,12 @@ +"""Butler Type models.""" + +from ..models.common import CaseInsensitiveEnum + + +class ButlerType(str, CaseInsensitiveEnum): + """Enumeration of possible butler types.""" + + __slots__ = () + + DIRECT = "DIRECT" + REMOTE = "REMOTE" diff --git a/src/sia/models/common.py b/src/sia/models/common.py new file mode 100644 index 0000000..65409fd --- /dev/null +++ b/src/sia/models/common.py @@ -0,0 +1,37 @@ +"""Common models used by in different places in the application.""" + +from enum import Enum +from typing import TypeVar + +T = TypeVar("T", bound="CaseInsensitiveEnum") + +__all__ = ["CaseInsensitiveEnum"] + + +class CaseInsensitiveEnum(Enum): + """A case-insensitive Enum class. + This class allows for case-insensitive comparisons of Enum values. + """ + + @classmethod + def _missing_(cls: type[T], value: object) -> T | None: + """Return the Enum member that matches the given value. + + Parameters + ---------- + value + The value to match. + + Returns + ------- + Enum + The Enum member that matches the given value. + """ + if not isinstance(value, str): + return None + + for member in cls: + if member.value.lower() == value.lower(): + return member + + raise ValueError(f"{value!r} is not a valid {cls.__name__}") diff --git a/src/sia/models/data_collections.py b/src/sia/models/data_collections.py new file mode 100644 index 0000000..be07948 --- /dev/null +++ b/src/sia/models/data_collections.py @@ -0,0 +1,125 @@ +"""Data collection models.""" + +import contextlib +from dataclasses import dataclass +from pathlib import Path +from typing import Annotated + +from lsst.daf.butler import ButlerConfig +from lsst.dax.obscore import ExporterConfig +from pydantic import Field, HttpUrl + +from ..models.butler_type import ButlerType + + +@dataclass +class ButlerDataCollection: + """Model to represent a Remote Butler data collection.""" + + config: Annotated[ + HttpUrl | Path, + Field( + description="Config Path or URL to obscore config for collection", + examples=[ + "https://example.com/butler-repo/path/to/local/dp02.yaml", + "/path/to/local/butler/dp02.yaml", + ], + ), + ] + """The obscore configuration file for this data collection.""" + + repository: Annotated[ + HttpUrl | Path, + Field( + description="Butler Repository Path or URL", + examples=[ + "https://example.com/butler-repo/path/to/local/repository", + "/path/to/local/butler/repository", + ], + ), + ] + """Butler Repository Path or URL""" + + label: Annotated[ + str, + Field( + description="The label for this Butler collection. Used to " + "identify the collection in the case where we are " + "using a Remote Butler", + examples=["LSST.DP02"], + ), + ] + """The label for this Butler collection""" + + name: Annotated[ + str, + Field( + description="The name for this Butler collection. This value is " + "used to identify the collection in API URLs. " + "For example, a name of 'dp02' would be used in the URL " + "'/api/sia/dp02/query'.", + examples=["dp02"], + ), + ] + """The name for this Butler collection.""" + + butler_type: Annotated[ + ButlerType, + Field( + description="The Butler type for this data collection.", + examples=["REMOTE", "DIRECT"], + ), + ] + """The Butler type for this data collection.""" + + default_instrument: Annotated[ + str, + Field( + default=None, + description="The default instrument for the data collection", + ), + ] + """The default instrument for the data collection""" + + datalink_url: Annotated[ + HttpUrl | None, + Field( + default=None, + description="An optional datalink URL to use instead of the one " + "in the config. This will overwrite the value in the obscore " + "configuration for the collection", + ), + ] = None + """An optional datalink URL to use instead of the one in the config""" + + @property + def identifier(self) -> str: + """Get the identifier for the data collection. + + Returns + ------- + str + The identifier. + """ + return f"{self.label}:{self.repository}" + + def get_exporter_config(self) -> ExporterConfig: + """Get the exporter configuration. + + Returns + ------- + ExporterConfig + The exporter configuration. + """ + config_data = ButlerConfig(str(self.config)) + exporter_config = ExporterConfig.model_validate(config_data) + # Overwrite datalink format if provided + for name in exporter_config.dataset_types: + with contextlib.suppress(AttributeError): + # We normally should find the datalink_url_fmt attribute + # If it doesn't exist this doesn't seem to be a critical issue + # so we suppress the AttributeError + exporter_config.dataset_types[name].datalink_url_fmt = str( + self.datalink_url + ) + return exporter_config diff --git a/src/vosiav2/models.py b/src/sia/models/index.py similarity index 92% rename from src/vosiav2/models.py rename to src/sia/models/index.py index 3dee16a..ff4830b 100644 --- a/src/vosiav2/models.py +++ b/src/sia/models/index.py @@ -1,4 +1,4 @@ -"""Models for vo-siav2.""" +"""Data models for the top-level route.""" from pydantic import BaseModel, Field from safir.metadata import Metadata as SafirMetadata diff --git a/src/sia/models/sia_query_params.py b/src/sia/models/sia_query_params.py new file mode 100644 index 0000000..a55bd6c --- /dev/null +++ b/src/sia/models/sia_query_params.py @@ -0,0 +1,383 @@ +"""SIA query parameters models.""" + +from abc import ABC, abstractmethod +from collections.abc import Iterable +from dataclasses import asdict, dataclass +from enum import Enum +from numbers import Integral +from typing import Annotated, Any, Self, TypeVar, cast + +from fastapi import Query +from lsst.dax.obscore.siav2 import SIAv2Parameters + +from ..exceptions import UsageFaultError +from ..models.common import CaseInsensitiveEnum + +__all__ = [ + "BaseQueryParams", + "SIAQueryParams", + "Shape", + "DPType", + "Polarization", + "CalibLevel", +] + +T = TypeVar("T", bound=Enum) + + +class CalibLevel(int, Enum): + """Enumeration of possible calibration levels.""" + + LEVEL0 = 0 + LEVEL1 = 1 + LEVEL2 = 2 + LEVEL3 = 3 + + +class Shape(CaseInsensitiveEnum): + """Enumeration of possible shapes for the POS parameter.""" + + CIRCLE = "CIRCLE" + RANGE = "RANGE" + POLYGON = "POLYGON" + + +class DPType(CaseInsensitiveEnum): + """Enumeration of possible data product types.""" + + IMAGE = "IMAGE" + CUBE = "CUBE" + + +class Polarization(CaseInsensitiveEnum): + """Enumeration of possible polarization states.""" + + I = "I" # noqa: E741 + Q = "Q" + U = "U" + V = "V" + RR = "RR" + LL = "LL" + RL = "RL" + LR = "LR" + XX = "XX" + YY = "YY" + XY = "XY" + YX = "YX" + + +class BaseQueryParams(ABC): + """Base class for query parameters.""" + + @abstractmethod + def to_butler_parameters(self) -> Any: + """Convert the query parameters vto the format expected by the + specific Butler type. + """ + + +@dataclass +class SIAQueryParams(BaseQueryParams): + """A class to represent the parameters for an SIA query. + + Attributes + ---------- + pos + Positional region(s) to be searched. + format + Image format(s). + time + Time interval(s) to be searched. + band + Band interval(s) to be searched. + pol + Polarization state(s) to be searched. + fov + Range(s) of field of view. + spatres + Range(s) of spatial resolution. + exptime + Range(s) of exposure times. + timeres + Range(s) of temporal resolution. + specrp + Range(s) of spectral resolving power. + id + Identifier of dataset(s). (Case insensitive) + dptype + Type of data (dataproduct_type). + calib + Calibration level of the data. + target + Name of the target. + collection + Name of the data collection. + facility + Name of the facility. + instrument + Name of the instrument. + maxrec + Maximum number of records in the response. + responseformat + Format of the response. + + Notes + ----- + I have tried using Pydantic here, but as I understand there is currently + a limitation with how FastAPI handles Pydantic models for query parameters + with list attributes + (See: https://github.com/fastapi/fastapi/discussions/10556) + """ + + pos: Annotated[ + list[str] | None, + Query( + title="pos", + description="Positional region(s) to be searched", + examples=["55.7467 -32.2862 0.05"], + ), + ] = None + + format: Annotated[ + list[str] | None, + Query( + title="format", + alias="format", + description="Response format(s)", + examples=["application/x-votable+xml"], + ), + ] = None + + time: Annotated[ + list[str] | None, + Query( + title="time", + description="Time interval(s) to be searched", + examples=["60550.31803461111 60550.31838182871"], + ), + ] = None + + band: Annotated[ + list[str] | None, + Query( + title="band", + description="Energy interval(s) to be searched", + examples=["0.1 10.0"], + ), + ] = None + + pol: Annotated[ + list[Polarization] | None, + Query( + title="pol", + description="Polarization state(s) to be searched", + examples=["I", "Q"], + ), + ] = None + + fov: Annotated[ + list[str] | None, + Query( + title="fov", + description="Range(s) of field of view", + examples=["1.0 2.0"], + ), + ] = None + + spatres: Annotated[ + list[str] | None, + Query( + title="spatres", + description="Range(s) of spatial resolution", + examples=["0.1 0.2"], + ), + ] = None + + exptime: Annotated[ + list[str] | None, + Query( + title="exptime", + description="Range(s) of exposure times", + examples=["-Inf 60"], + ), + ] = None + + timeres: Annotated[ + list[str] | None, + Query( + title="timeres", + description="Range(s) of temporal resolution", + examples=["-Inf 1.0"], + ), + ] = None + + specrp: Annotated[ + list[str] | None, + Query( + title="specrp", + description="Range(s) of spectral resolving power", + examples=["1000 2000"], + ), + ] = None + + id: Annotated[ + list[str] | None, + Query( + title="id", + alias="id", + description="Identifier of dataset(s)", + examples=["obs_id_1"], + ), + ] = None + + dptype: Annotated[ + list[DPType] | None, + Query(title="dptype", description="Type of data", examples=["image"]), + ] = None + + calib: Annotated[ + list[CalibLevel] | None, + Query( + title="calib", + description="Calibration level of the data", + examples=[0, 1, 2], + ), + ] = None + + target: Annotated[ + list[str] | None, + Query( + title="target", description="Name of the target", examples=["M31"] + ), + ] = None + + collection: Annotated[ + list[str] | None, + Query( + title="collection", + description="Name of the data collection", + examples=["HST"], + ), + ] = None + + facility: Annotated[ + list[str] | None, + Query( + title="facility", + description="Name of the facility", + examples=["HST"], + ), + ] = None + + instrument: Annotated[ + list[str] | None, + Query( + title="instrument", + description="Name of the instrument", + examples=["ACS"], + ), + ] = None + + maxrec: Annotated[ + int | None, + Query( + title="maxrec", + description="Maximum number of records in the response", + examples=[10], + ), + ] = None + + responseformat: Annotated[ + str | None, + Query( + title="reponseformat", + description="Format of the response", + examples=["application/x-votable+xml"], + ), + ] = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Self: + """Create an instance of SIAQueryParams from a dictionary. + + Parameters + ---------- + data + The dictionary containing the query parameters. + + Returns + ------- + SIAQueryParams + Instance of SIAQueryParams initialized with the provided data. + """ + return cls(**data) + + def all_params_none(self) -> bool: + """Check if all params except maxrec and responseformat are None.""" + return all( + getattr(self, attr) is None + for attr in self.__annotations__ + if attr not in ["maxrec", "responseformat"] + ) + + def __post_init__(self) -> None: + """Validate the form parameters.""" + # If no parameters were provided, I don't think we should run a query + # Instead return the self-description VOTable + if self.all_params_none(): + self.maxrec = 0 + + def to_dict(self) -> dict[str, Any]: + """Return the query parameters as a dictionary. + + Returns + ------- + dict + The query parameters as a dictionary. + """ + return {k: v for k, v in asdict(self).items() if v is not None} + + def to_butler_parameters(self) -> SIAv2Parameters: + """Convert the query parameters to SIAv2Parameters. Exclude None + values. + + Returns + ------- + SIAv2Parameters + The query parameters as a dictionary. + + Raises + ------ + UsageFaultError + If the query parameters are invalid. + """ + try: + return SIAv2Parameters.from_siav2( + instrument=self.instrument or (), + pos=self.pos or (), + time=self.time or (), + band=self.band or (), + exptime=self.exptime or (), + calib=self._convert_calib(calib=self.calib), + maxrec=str(self.maxrec) if self.maxrec is not None else None, + ) + except ValueError as exc: + raise UsageFaultError(detail=str(exc)) from exc + + @staticmethod + def _convert_calib(calib: list[CalibLevel] | None) -> Iterable[Integral]: + """Convert the calibration levels to integers. + + Parameters + ---------- + calib + The calibration levels. + + Returns + ------- + Iterable + The calibration levels as integers. + """ + if calib is None: + return () + return cast(list[Integral], [int(level.value) for level in calib]) diff --git a/src/sia/services/__init__.py b/src/sia/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sia/services/availability.py b/src/sia/services/availability.py new file mode 100644 index 0000000..a422cba --- /dev/null +++ b/src/sia/services/availability.py @@ -0,0 +1,115 @@ +"""Service for checking the availability of the system.""" + +from abc import ABC, abstractmethod + +from httpx import AsyncClient +from vo_models.vosi.availability import Availability + +from ..exceptions import FatalFaultError +from ..models.butler_type import ButlerType +from ..models.data_collections import ButlerDataCollection + + +class AvailabilityChecker(ABC): + """Base class for availability checkers.""" + + @abstractmethod + async def check_availability( + self, *, collection: ButlerDataCollection + ) -> Availability: + """Check the availability of the service. + + Parameters + ---------- + collection + The ButlerDataCollection instance + + Returns + ------- + Availability + The availability of the service. + """ + + +class DirectButlerAvailabilityChecker(AvailabilityChecker): + """Checker for the availability of the direct Butler based service.""" + + async def check_availability( + self, *, collection: ButlerDataCollection + ) -> Availability: + """Check the availability of the direct Butler based service. + For now this just returns Available(True). We could improve this by + checking if we can create a valid Butler with the config provided. + + Parameters + ---------- + collection + The ButlerDataCollection instance + + Returns + ------- + Availability + The availability of the service if using direct Butler. + """ + return Availability(available=True) + + +class RemoteButlerAvailabilityChecker(AvailabilityChecker): + """Checker for the availability of a remote Butler based service.""" + + async def check_availability( + self, *, collection: ButlerDataCollection + ) -> Availability: + """Check the availability of a remote Butler based service. + This checks if the remote Butler is available by sending a GET request + to the root URL of the remote Butler. + + Parameters + ---------- + collection + The ButlerDataCollection instance + + Returns + ------- + Availability + The availability of the remote Butler based service. + """ + async with AsyncClient() as client: + try: + repository = collection.repository + r = await client.get(str(repository)) if repository else None + if not r: + return Availability(available=False) + + return Availability(available=r.status_code == 200) + + except (KeyError, ValueError, FatalFaultError) as exc: + return Availability(note=[str(exc)], available=False) + + +class AvailabilityService: + """Service for checking the availability of the system.""" + + def __init__(self, *, collection: ButlerDataCollection) -> None: + self._collection = collection + self._checkers: dict[ButlerType, AvailabilityChecker] = { + ButlerType.DIRECT: DirectButlerAvailabilityChecker(), + ButlerType.REMOTE: RemoteButlerAvailabilityChecker(), + } + + async def get_availability(self) -> Availability: + """Check the availability of the system. + + Returns + ------- + Availability + The availability of the service. + """ + butler_type = self._collection.butler_type + checker = self._checkers.get(butler_type) + if checker: + return await checker.check_availability( + collection=self._collection + ) + else: + return Availability(note=["Unknown Butler type"], available=False) diff --git a/src/sia/services/data_collections.py b/src/sia/services/data_collections.py new file mode 100644 index 0000000..94c08bd --- /dev/null +++ b/src/sia/services/data_collections.py @@ -0,0 +1,133 @@ +"""Data collection helper service.""" + +from dataclasses import dataclass + +from ..config import Config +from ..models.data_collections import ButlerDataCollection + + +@dataclass +class DataCollectionService: + """Data Collection service class.""" + + config: Config + """The configuration object for the data collection.""" + + def _get_data_collection( + self, + *, + key: str, + value: str, + attribute: str, + ) -> ButlerDataCollection: + """Return the Data collection for the given attribute and value. + + Parameters + ---------- + key + The name of the attribute being searched (for error messages). + value + The value to search for. + attribute + The attribute of ButlerDataCollection to match against. + + Returns + ------- + ButlerDataCollection + The Butler Data collection. + + Raises + ------ + ValueError + If the value is empty. + KeyError + If the value is not found in the Data collections. + """ + if not value: + raise ValueError(f"{key.capitalize()} is required.") + + for collection in self.config.butler_data_collections: + if getattr(collection, attribute).upper() == value.upper(): + return collection + + raise KeyError( + f"{key.capitalize()} {value} not found in Data collections." + ) + + def get_data_collection_by_label( + self, + *, + label: str, + ) -> ButlerDataCollection: + """Return the Data collection URL for the given label. + + Parameters + ---------- + label + The label of the data collection. + + Returns + ------- + ButlerDataCollection + The Butler Data collection. + + Raises + ------ + ValueError + No label was provided + KeyError + If the label is not found in the Data collections. + """ + return self._get_data_collection( + key="label", value=label, attribute="label" + ) + + def get_data_collection_by_name( + self, + *, + name: str, + ) -> ButlerDataCollection: + """Return the Data collection URL for the given name. + + Parameters + ---------- + name + The name of the data collection. + + Returns + ------- + ButlerDataCollection + The Butler Data collection. + + Raises + ------ + KeyError + If the label is not found in the Data collections. + + UsageFaultError + If the label is not found in the Data collections. + """ + return self._get_data_collection( + key="name", value=name, attribute="name" + ) + + def get_data_repositories(self) -> dict[str, str]: + """Read the Data repositories from config and return a dictionary + mapping labels to repository URLs. + This is used to populate the LabeledButlerFactory. + + Returns + ------- + dict + A dictionary mapping labels to repository URLs. + """ + butler_repos = {} + + for collection in self.config.butler_data_collections: + label = collection.label + repository = collection.repository + + if label and repository: + butler_repos[label] = str(repository) + + return butler_repos diff --git a/src/sia/services/response_handler.py b/src/sia/services/response_handler.py new file mode 100644 index 0000000..3f5bd3f --- /dev/null +++ b/src/sia/services/response_handler.py @@ -0,0 +1,91 @@ +"""Module for the Query Processor service.""" + +from collections.abc import Callable + +import astropy +import structlog +from fastapi import Request +from lsst.daf.butler import Butler +from lsst.dax.obscore import ExporterConfig +from lsst.dax.obscore.siav2 import SIAv2Parameters +from starlette.responses import Response + +from ..constants import RESULT_NAME as RESULT +from ..factory import Factory +from ..models.data_collections import ButlerDataCollection +from ..services.votable import VotableConverterService + +logger = structlog.get_logger(__name__) + +SIAv2QueryType = Callable[ + [Butler, ExporterConfig, SIAv2Parameters], + astropy.io.votable.tree.VOTableFile, +] + + +class ResponseHandlerService: + """Service for handling the SIAv2 query response.""" + + @staticmethod + def process_query( + *, + factory: Factory, + params: SIAv2Parameters, + sia_query: SIAv2QueryType, + request: Request, + collection: ButlerDataCollection, + token: str | None, + ) -> Response: + """Process the SIAv2 query and generate a Response. + + Parameters + ---------- + factory + The Factory instance. + params + The parameters for the SIAv2 query. + sia_query + The SIA query method to use + request + The request object. + collection + The Butler data collection + token + The token to use for the Butler (Optional). + + Returns + ------- + Response + The response containing the query results. + """ + logger.info( + "SIA query started with params:", + params=params, + method=request.method, + ) + + butler = factory.create_butler( + butler_collection=collection, + token=token, + ) + + # Execute the query + table_as_votable = sia_query( + butler, + collection.get_exporter_config(), + params, + ) + + # Convert the result to a string + result = VotableConverterService(table_as_votable).to_string() + + # For the moment only VOTable is supported, so we can hardcode the + # media_type and the file extension. + return Response( + headers={ + "content-disposition": f"attachment; filename={RESULT}.xml", + "Content-Type": "application/x-votable+xml", + }, + content=result, + media_type="application/x-votable+xml", + ) diff --git a/src/sia/services/votable.py b/src/sia/services/votable.py new file mode 100644 index 0000000..08cef34 --- /dev/null +++ b/src/sia/services/votable.py @@ -0,0 +1,35 @@ +"""Module with relevant class needed to convert a VOTableFile object to a +string. +""" + +import io + +from astropy.io.votable.tree import VOTableFile + +__all__ = ["VotableConverterService"] + + +class VotableConverterService: + """A class to convert a VOTableFile object to a string. + + Attributes + ---------- + votable + The VOTableFile object to convert. + """ + + def __init__(self, votable: VOTableFile) -> None: + self.votable = votable + + def to_string(self) -> str: + """Convert the VOTableFile object to a string. + + Returns + ------- + str + The VOTableFile object as a string. + + """ + with io.BytesIO() as output: + self.votable.to_xml(output) + return output.getvalue().decode("utf-8") diff --git a/src/sia/templates/availability.xml b/src/sia/templates/availability.xml new file mode 100644 index 0000000..baf88b1 --- /dev/null +++ b/src/sia/templates/availability.xml @@ -0,0 +1,3 @@ + +true + \ No newline at end of file diff --git a/src/sia/templates/capabilities.xml b/src/sia/templates/capabilities.xml new file mode 100644 index 0000000..5a4a3ca --- /dev/null +++ b/src/sia/templates/capabilities.xml @@ -0,0 +1,25 @@ + + + + + {{ capabilities_url }} + + + + + {{ availability_url }} + + + + + + + + + + {{ query_url }} + + + \ No newline at end of file diff --git a/src/sia/templates/votable_error.xml b/src/sia/templates/votable_error.xml new file mode 100644 index 0000000..8ff35b2 --- /dev/null +++ b/src/sia/templates/votable_error.xml @@ -0,0 +1,6 @@ + + + + {{ error_message }} + + \ No newline at end of file diff --git a/src/vosiav2/config.py b/src/vosiav2/config.py deleted file mode 100644 index 1a2ec2c..0000000 --- a/src/vosiav2/config.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Configuration definition.""" - -from __future__ import annotations - -from pydantic import Field -from pydantic_settings import BaseSettings, SettingsConfigDict -from safir.logging import LogLevel, Profile - -__all__ = ["Config", "config"] - - -class Config(BaseSettings): - """Configuration for vo-siav2.""" - - name: str = Field("vo-siav2", title="Name of application") - - path_prefix: str = Field( - "/vo-siav2", title="URL prefix for application" - ) - - profile: Profile = Field( - Profile.development, title="Application logging profile" - ) - - log_level: LogLevel = Field( - LogLevel.INFO, title="Log level of the application's logger" - ) - - model_config = SettingsConfigDict( - env_prefix="VO_SIAV2_", case_sensitive=False - ) - - -config = Config() -"""Configuration for vo-siav2.""" diff --git a/src/vosiav2/handlers/external.py b/src/vosiav2/handlers/external.py deleted file mode 100644 index ebaad52..0000000 --- a/src/vosiav2/handlers/external.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Handlers for the app's external root, ``/vo-siav2/``.""" - -from typing import Annotated - -from fastapi import APIRouter, Depends -from safir.dependencies.logger import logger_dependency -from safir.metadata import get_metadata -from structlog.stdlib import BoundLogger - -from ..config import config -from ..models import Index - -__all__ = ["get_index", "external_router"] - -external_router = APIRouter() -"""FastAPI router for all external handlers.""" - - -@external_router.get( - "/", - description=( - "Document the top-level API here. By default it only returns metadata" - " about the application." - ), - response_model=Index, - response_model_exclude_none=True, - summary="Application metadata", -) -async def get_index( - logger: Annotated[BoundLogger, Depends(logger_dependency)], -) -> Index: - """GET ``/vo-siav2/`` (the app's external root). - - Customize this handler to return whatever the top-level resource of your - application should return. For example, consider listing key API URLs. - When doing so, also change or customize the response model in - `vosiav2.models.Index`. - - By convention, the root of the external API includes a field called - ``metadata`` that provides the same Safir-generated metadata as the - internal root endpoint. - """ - # There is no need to log simple requests since uvicorn will do this - # automatically, but this is included as an example of how to use the - # logger for more complex logging. - logger.info("Request for application metadata") - - metadata = get_metadata( - package_name="vo-siav2", - application_name=config.name, - ) - return Index(metadata=metadata) diff --git a/src/vosiav2/main.py b/src/vosiav2/main.py deleted file mode 100644 index f738168..0000000 --- a/src/vosiav2/main.py +++ /dev/null @@ -1,60 +0,0 @@ -"""The main application factory for the vo-siav2 service. - -Notes ------ -Be aware that, following the normal pattern for FastAPI services, the app is -constructed when this module is loaded and is not deferred until a function is -called. -""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager -from importlib.metadata import metadata, version - -from fastapi import FastAPI -from safir.dependencies.http_client import http_client_dependency -from safir.logging import configure_logging, configure_uvicorn_logging -from safir.middleware.x_forwarded import XForwardedMiddleware - -from .config import config -from .handlers.external import external_router -from .handlers.internal import internal_router - -__all__ = ["app"] - - -@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. - - yield - - # Any code here will be run when the application shuts down. - await http_client_dependency.aclose() - - -configure_logging( - profile=config.profile, - log_level=config.log_level, - name="vosiav2", -) -configure_uvicorn_logging(config.log_level) - -app = FastAPI( - title="vo-siav2", - description=metadata("vo-siav2")["Summary"], - version=version("vo-siav2"), - openapi_url=f"{config.path_prefix}/openapi.json", - docs_url=f"{config.path_prefix}/docs", - redoc_url=f"{config.path_prefix}/redoc", - lifespan=lifespan, -) -"""The main FastAPI application for vo-siav2.""" - -# Attach the routers. -app.include_router(internal_router) -app.include_router(external_router, prefix=f"{config.path_prefix}") - -# Add middleware. -app.add_middleware(XForwardedMiddleware) diff --git a/tests/conftest.py b/tests/conftest.py index 6b13e5b..c512359 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,24 +1,75 @@ -"""Test fixtures for vo-siav2 tests.""" +"""Test fixtures for sia tests.""" from __future__ import annotations -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Iterator +from pathlib import Path +from unittest.mock import AsyncMock, Mock +import pytest import pytest_asyncio from asgi_lifespan import LifespanManager -from fastapi import FastAPI +from fastapi import FastAPI, Request from httpx import ASGITransport, AsyncClient +from pydantic import HttpUrl -from vosiav2 import main +from sia import main +from sia.config import Config, config +from sia.models.butler_type import ButlerType +from sia.models.data_collections import ButlerDataCollection + +from .support.butler import ( + MockButler, + MockButlerQueryService, + patch_butler, + patch_siav2_query, +) + +BASE_PATH = Path(__file__).parent @pytest_asyncio.fixture -async def app() -> AsyncIterator[FastAPI]: +async def app(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[FastAPI]: """Return a configured test application. Wraps the application in a lifespan manager so that startup and shutdown events are sent during test execution. """ + + @main.app.post("/test-params") + async def test_params_endpoint( + request: Request, + ) -> dict: + """Test endpoint for the middleware. + + Parameters + ---------- + request + The incoming request + + Returns + ------- + dict[str, dict[str, str]] + The response data + """ + form_data = await request.form() + return {"method": "POST", "form_data": dict(form_data)} + + data_config = BASE_PATH / "data" / "config" / "dp02.yaml" + + butler_collections = [ + ButlerDataCollection( + config=data_config, + label="LSST.DP02", + name="dp02", + butler_type=ButlerType.REMOTE, + repository=HttpUrl("https://example/repo/dp02/butler.yaml"), + default_instrument="LSSTCam-imSim", + ), + ] + monkeypatch.setattr(config, "path_prefix", "/api/sia") + monkeypatch.setattr(config, "butler_data_collections", butler_collections) + async with LifespanManager(main.app): yield main.app @@ -27,7 +78,183 @@ async def app() -> AsyncIterator[FastAPI]: async def client(app: FastAPI) -> AsyncIterator[AsyncClient]: """Return an ``httpx.AsyncClient`` configured to talk to the test app.""" async with AsyncClient( - transport=ASGITransport(app=app), # type: ignore[arg-type] + transport=ASGITransport(app=app), + base_url="https://example.com/", + headers={"X-Auth-Request-Token": "sometoken"}, + ) as client: + yield client + + +@pytest_asyncio.fixture +async def app_direct( + monkeypatch: pytest.MonkeyPatch, +) -> AsyncIterator[FastAPI]: + """Return a configured test application. + + Wraps the application in a lifespan manager so that startup and shutdown + events are sent during test execution. + """ + repo_path = BASE_PATH / "data" / "repo" + config_file = BASE_PATH / "data" / "config" / "ci_hsc_gen3.yaml" + + butler_collections = [ + ButlerDataCollection( + config=config_file, + repository=repo_path, + butler_type=ButlerType.DIRECT, + label="ci_hsc_gen3", + name="hsc", + default_instrument="HSC", + ), + ] + + monkeypatch.setattr(config, "path_prefix", "/api/sia") + monkeypatch.setattr(config, "butler_data_collections", butler_collections) + + async with LifespanManager(main.app): + yield main.app + + +@pytest_asyncio.fixture +async def client_direct(app_direct: FastAPI) -> AsyncIterator[AsyncClient]: + """Return an ``httpx.AsyncClient`` configured to talk to the test app.""" + async with AsyncClient( + transport=ASGITransport(app=app_direct), base_url="https://example.com/", ) as client: yield client + + +@pytest_asyncio.fixture +async def expected_votable() -> str: + """Return the expected VOTable content as a string.""" + xml_file_path = BASE_PATH / "templates" / "expected_votable.xml" + with xml_file_path.open("r", encoding="utf-8") as file: + return file.read() + + +@pytest_asyncio.fixture +async def test_config_remote() -> Config: + """Return a test configuration for a remote Butler. + + Returns + ------- + Config + The test configuration + """ + config = BASE_PATH / "data" / "config" / "dp02.yaml" + + butler_collections = [ + ButlerDataCollection( + config=config, + label="LSST.DP02", + name="dp02", + repository=HttpUrl( + "https://example.com/api/butler/repo/dp02/butler" ".yaml" + ), + butler_type=ButlerType.REMOTE, + default_instrument="LSSTCam-imSim", + ), + ] + return Config( + path_prefix="/api/sia", + butler_data_collections=butler_collections, + ) + + +@pytest_asyncio.fixture +async def test_config_direct() -> Config: + """Return a test configuration for a direct Butler. + + Returns + ------- + Config + The test configuration + """ + base_path = Path(__file__).parent + repo_path = base_path / "data" / "repo" + config_file = base_path / "data" / "config" / "ci_hsc_gen3.yaml" + + butler_collections = [ + ButlerDataCollection( + config=config_file, + repository=repo_path, + butler_type=ButlerType.DIRECT, + label="ci_hsc_gen3", + name="hsc", + default_instrument="LSSTCam-imSim", + ), + ] + + return Config( + butler_data_collections=butler_collections, + ) + + +@pytest.fixture +def mock_async_client( + monkeypatch: pytest.MonkeyPatch, +) -> tuple[AsyncMock, AsyncMock]: + """Return a mock AsyncClient and a mock response object. + + Parameters + ---------- + monkeypatch + The pytest monkeypatch fixture + + Returns + ------- + tuple[AsyncMock, AsyncMock] + The mock AsyncClient and mock response objects + """ + mock_response = AsyncMock() + mock_response.status_code = 200 + + mock_client = AsyncMock(spec=AsyncClient) + mock_client.__aenter__.return_value.get.return_value = mock_response + + monkeypatch.setattr( + "sia.services.availability.AsyncClient", lambda: mock_client + ) + + return mock_client, mock_response + + +@pytest.fixture +def mock_exporter_config(monkeypatch: pytest.MonkeyPatch) -> Mock: + """Return a mock ExporterConfig instance.""" + mock = Mock() + monkeypatch.setattr( + "sia.factories.butler_type_factory.ExporterConfig", mock + ) + return mock + + +@pytest.fixture +def mock_labeled_butler_factory(monkeypatch: pytest.MonkeyPatch) -> Mock: + """Return a mock LabeledButlerFactory instance.""" + mock = Mock() + monkeypatch.setattr( + "sia.factories.butler_type_factory.LabeledButlerFactory", mock + ) + return mock + + +@pytest.fixture +def mock_butler_config(monkeypatch: pytest.MonkeyPatch) -> Mock: + """Return a mock ButlerConfig instance.""" + mock = Mock() + monkeypatch.setattr("sia.factories.butler_type_factory.ButlerConfig", mock) + return mock + + +@pytest.fixture +def mock_siav2_query() -> Iterator[MockButlerQueryService]: + """Mock Butler for testing.""" + yield from patch_siav2_query() + + +@pytest.fixture +def mock_butler() -> Iterator[MockButler]: + """Mock Butler for testing.""" + yield from patch_butler() diff --git a/tests/data/config/ci_hsc_gen3.yaml b/tests/data/config/ci_hsc_gen3.yaml new file mode 100644 index 0000000..0f0a4d7 --- /dev/null +++ b/tests/data/config/ci_hsc_gen3.yaml @@ -0,0 +1,58 @@ +facility_name: Subaru +obs_collection: LSST.CI +collections: ["HSC/runs/ci_hsc"] +dataset_types: + raw: + dataproduct_type: image + dataproduct_subtype: lsst.raw + calib_level: 1 + obs_id_fmt: "{records[exposure].obs_id}" + o_ucd: phot.count + access_format: image/fits + calexp: + dataproduct_type: image + dataproduct_subtype: lsst.calexp + calib_level: 2 + obs_id_fmt: "{records[visit].name}" + o_ucd: phot.count + access_format: image/fits + deepCoadd: + dataproduct_type: image + dataproduct_subtype: lsst.coadd + calib_level: 3 + obs_id_fmt: "{skymap}-{tract}-{patch}" + o_ucd: phot.count + access_format: image/fits +extra_columns: + lsst_visit: + template: "{visit}" + type: "int" + lsst_detector: + template: "{detector}" + type: "int" + lsst_tract: + template: "{tract}" + type: "int" + lsst_patch: + template: "{patch}" + type: "int" + lsst_band: + template: "{band}" + type: "string" + lsst_filter: + template: "{physical_filter}" + type: "string" +spectral_ranges: + "HSC-G": [406.0e-9, 545.0e-9] + "HSC-R": [543.0e-9, 693.0e-9] + "HSC-R2": [542.0e-9, 693.0e-9] + "HSC-I": [690.0e-9, 842.0e-9] + "HSC-I2": [692.0e-9, 850.0e-9] + "HSC-Z": [852.0e-9, 928.0e-9] + "HSC-Y": [937.0e-9, 1015.0e-9] + "N921": [914.7e-9, 928.1e-9] + "g": [406.0e-9, 545.0e-9] + "r": [542.0e-9, 693.0e-9] + "i": [692.0e-9, 850.0e-9] + "z": [852.0e-9, 928.0e-9] + "y": [937.0e-9, 1015.0e-9] diff --git a/tests/data/config/dp02.yaml b/tests/data/config/dp02.yaml new file mode 100644 index 0000000..407a15e --- /dev/null +++ b/tests/data/config/dp02.yaml @@ -0,0 +1,53 @@ +facility_name: Rubin-LSST +obs_collection: LSST.DP02 +collections: ["2.2i/runs/DP0.2"] +use_butler_uri: false +dataset_types: + raw: + dataproduct_type: image + dataproduct_subtype: lsst.raw + calib_level: 1 + obs_id_fmt: "{records[exposure].obs_id}-{records[detector].full_name}" + o_ucd: phot.count + access_format: image/fits + datalink_url_fmt: "https://data.lsst.cloud/api/datalink/links?ID=butler%3A//dp02/{id}" + calexp: + dataproduct_type: image + dataproduct_subtype: lsst.calexp + calib_level: 2 + obs_id_fmt: "{records[visit].name}-{records[detector].full_name}" + o_ucd: phot.count + access_format: image/fits + datalink_url_fmt: "https://data.lsst.cloud/api/datalink/links?ID=butler%3A//dp02/{id}" + deepCoadd_calexp: + dataproduct_type: image + dataproduct_subtype: lsst.deepCoadd_calexp + calib_level: 3 + obs_id_fmt: "{skymap}-{tract}-{patch}" + o_ucd: phot.count + access_format: image/fits + datalink_url_fmt: "https://data.lsst.cloud/api/datalink/links?ID=butler%3A//dp02/{id}" + goodSeeingCoadd: + dataproduct_type: image + dataproduct_subtype: lsst.goodSeeingCoadd + calib_level: 3 + obs_id_fmt: "{skymap}-{tract}-{patch}" + o_ucd: phot.count + access_format: image/fits + datalink_url_fmt: "https://data.lsst.cloud/api/datalink/links?ID=butler%3A//dp02/{id}" + goodSeeingDiff_differenceExp: + dataproduct_type: image + dataproduct_subtype: lsst.goodSeeingDiff_differenceExp + calib_level: 3 + obs_id_fmt: "{records[visit].name}-{records[detector].full_name}" + o_ucd: phot.count + access_format: image/fits + datalink_url_fmt: "https://data.lsst.cloud/api/datalink/links?ID=butler%3A//dp02/{id}" + +spectral_ranges: + "u": [330.0e-9, 400.0e-9] + "g": [402.0e-9, 552.0e-9] + "r": [552.0e-9, 691.0e-9] + "i": [691.0e-9, 818.0e-9] + "z": [818.0e-9, 922.0e-9] + "y": [970.0e-9, 1060.0e-9] \ No newline at end of file diff --git a/tests/data/repo/butler.yaml b/tests/data/repo/butler.yaml new file mode 100644 index 0000000..4e63786 --- /dev/null +++ b/tests/data/repo/butler.yaml @@ -0,0 +1,14 @@ +datastore: + cls: lsst.daf.butler.datastores.fileDatastore.FileDatastore + records: + table: file_datastore_records + root: +registry: + db: sqlite:////gen3.sqlite3 + managers: + attributes: lsst.daf.butler.registry.attributes.DefaultButlerAttributeManager + collections: lsst.daf.butler.registry.collections.synthIntKey.SynthIntKeyCollectionManager + datasets: lsst.daf.butler.registry.datasets.byDimensions.ByDimensionsDatasetRecordStorageManagerUUID + datastores: lsst.daf.butler.registry.bridge.monolithic.MonolithicDatastoreRegistryBridgeManager + dimensions: lsst.daf.butler.registry.dimensions.static.StaticDimensionRecordStorageManager + opaque: lsst.daf.butler.registry.opaque.ByNameOpaqueTableStorageManager diff --git a/tests/data/repo/gen3.sqlite3 b/tests/data/repo/gen3.sqlite3 new file mode 100644 index 0000000..3d3ffb4 Binary files /dev/null and b/tests/data/repo/gen3.sqlite3 differ diff --git a/tests/exceptions/__init__.py b/tests/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/exceptions/exceptions_test.py b/tests/exceptions/exceptions_test.py new file mode 100644 index 0000000..5be96ed --- /dev/null +++ b/tests/exceptions/exceptions_test.py @@ -0,0 +1,47 @@ +"""Tests for the Exceptions module.""" + +import pytest +from fastapi import FastAPI + +from sia.errors import votable_exception_handler +from sia.exceptions import ( + DefaultFaultError, + FatalFaultError, + TransientFaultError, + UsageFaultError, +) + +app = FastAPI() +app.add_exception_handler(Exception, votable_exception_handler) + + +@pytest.mark.asyncio +async def test_usage_fault() -> None: + """Test the UsageFault exception.""" + exc = UsageFaultError("Test usage fault") + assert str(exc) == "UsageFault: Test usage fault" + assert exc.status_code == 400 + + +@pytest.mark.asyncio +async def test_transient_fault() -> None: + """Test the TransientFault exception.""" + exc = TransientFaultError("Test transient fault") + assert str(exc) == "TransientFault: Test transient fault" + assert exc.status_code == 400 + + +@pytest.mark.asyncio +async def test_fatal_fault() -> None: + """Test the FatalFault exception.""" + exc = FatalFaultError("Test fatal fault") + assert str(exc) == "FatalFault: Test fatal fault" + assert exc.status_code == 400 + + +@pytest.mark.asyncio +async def test_default_fault() -> None: + """Test the DefaultFault exception.""" + exc = DefaultFaultError("Test default fault") + assert str(exc) == "DefaultFault: Test default fault" + assert exc.status_code == 400 diff --git a/tests/handlers/external/__init__.py b/tests/handlers/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/handlers/external/availability_test.py b/tests/handlers/external/availability_test.py new file mode 100644 index 0000000..71a5c01 --- /dev/null +++ b/tests/handlers/external/availability_test.py @@ -0,0 +1,106 @@ +"""Test the availability's endpoint. +This test checks that the availability endpoint returns the +expected XML response, read from the templates/availability.xml file. +""" + +from pathlib import Path +from unittest.mock import AsyncMock, patch + +import pytest +from fastapi import APIRouter +from fastapi.templating import Jinja2Templates +from httpx import AsyncClient + +from sia.config import Config, config +from sia.services.availability import ( + AvailabilityService, + DirectButlerAvailabilityChecker, + RemoteButlerAvailabilityChecker, +) +from sia.services.data_collections import DataCollectionService + +router = APIRouter() +"""FastAPI router for all external handlers.""" + + +@pytest.mark.asyncio +async def test_availability( + client: AsyncClient, mock_async_client: AsyncMock +) -> None: + """Test the availability endpoint.""" + templates_dir = Jinja2Templates( + directory=str(Path(__file__).parent.parent.parent / "templates") + ) + mock_client, mock_response = mock_async_client + + r = await client.get(f"{config.path_prefix}/dp02/availability") + assert r.status_code == 200 + template_rendered = templates_dir.get_template("availability.xml").render() + assert r.text.strip() == template_rendered.strip() + + +@pytest.mark.asyncio +async def test_direct_butler_availability(test_config_direct: Config) -> None: + """Test the availability of the direct Butler .""" + collection = DataCollectionService( + config=test_config_direct + ).get_data_collection_by_name(name="hsc") + + checker = DirectButlerAvailabilityChecker() + availability = await checker.check_availability(collection=collection) + assert availability.available is True + + +@pytest.mark.asyncio +async def test_remote_butler_availability_success( + test_config_remote: Config, +) -> None: + """Test the availability of the remote Butler .""" + collection = DataCollectionService( + config=test_config_remote + ).get_data_collection_by_name(name="dp02") + + checker = RemoteButlerAvailabilityChecker() + with patch("sia.services.availability.AsyncClient") as mock_client: + mock_response = AsyncMock() + mock_response.status_code = 200 + mock_client.return_value.__aenter__.return_value.get.return_value = ( + mock_response + ) + availability = await checker.check_availability(collection=collection) + assert availability.available is True + + +@pytest.mark.asyncio +async def test_remote_butler_availability_failure( + test_config_remote: Config, +) -> None: + """Test the availability of the remote Butler when + it is not available. + """ + collection = DataCollectionService( + config=test_config_remote + ).get_data_collection_by_name(name="dp02") + + checker = RemoteButlerAvailabilityChecker() + with patch("sia.services.availability.AsyncClient") as mock_client: + mock_response = AsyncMock() + mock_response.status_code = 404 + mock_client.return_value.__aenter__.return_value.get.return_value = ( + mock_response + ) + + availability = await checker.check_availability(collection=collection) + assert availability.available is False + + +@pytest.mark.asyncio +async def test_availability_service(test_config_direct: Config) -> None: + """Test the availability service.""" + collection = DataCollectionService( + config=test_config_direct + ).get_data_collection_by_name(name="hsc") + + service = AvailabilityService(collection=collection) + availability = await service.get_availability() + assert availability.available is True diff --git a/tests/handlers/external/capabilities_test.py b/tests/handlers/external/capabilities_test.py new file mode 100644 index 0000000..5f0debf --- /dev/null +++ b/tests/handlers/external/capabilities_test.py @@ -0,0 +1,43 @@ +"""Test the capabilities' endpoint. +This test checks that the capabilities endpoint returns the +expected XML response, read from the templates/capabilities.xml file. +""" + +from pathlib import Path + +import pytest +from fastapi import APIRouter +from fastapi.templating import Jinja2Templates +from httpx import AsyncClient + +from sia.config import config + +router = APIRouter() +"""FastAPI router for all external handlers.""" + + +@pytest.mark.asyncio +async def test_capabilities(client: AsyncClient) -> None: + """Test the capabilities endpoint.""" + template_dir = str( + Path(__file__).resolve().parent.parent.parent / "templates" + ) + templates_dir = Jinja2Templates(template_dir) + + context = { + "capabilities_url": f"https://example.com" + f"{config.path_prefix}/dp02/capabilities", + "availability_url": f"https://example.com" + f"{config.path_prefix}/dp02/availability", + "query_url": f"https://example.com{config.path_prefix}/dp02/query", + } + + r = await client.get(f"{config.path_prefix}/dp02/capabilities") + assert r.status_code == 200 + template_rendered = templates_dir.get_template("capabilities.xml").render( + context + ) + + assert r.status_code == 200 + + assert r.text.strip() == template_rendered.strip() diff --git a/tests/handlers/external/external_test.py b/tests/handlers/external/external_test.py new file mode 100644 index 0000000..424602c --- /dev/null +++ b/tests/handlers/external/external_test.py @@ -0,0 +1,225 @@ +"""Tests for the sia.handlers.external module and routes.""" + +from __future__ import annotations + +import re +from typing import Any + +import pytest +from httpx import AsyncClient + +from sia.config import config +from sia.constants import RESULT_NAME +from tests.support.butler import MockButler, MockButlerQueryService +from tests.support.constants import EXCEPTION_MESSAGES +from tests.support.validators import validate_votable_error + + +@pytest.mark.asyncio +async def test_get_index(client: AsyncClient) -> None: + """Test ``GET /api/sia/``.""" + response = await client.get(f"{config.path_prefix}/") + assert response.status_code == 200 + data = response.json() + metadata = data["metadata"] + assert metadata["name"] == config.name + assert isinstance(metadata["version"], str) + assert isinstance(metadata["description"], str) + assert isinstance(metadata["repository_url"], str) + assert isinstance(metadata["documentation_url"], str) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ( + "query_params", + "expected_status", + "expected_content_type", + "expected_message", + ), + [ + ( + "POS=CIRCLE+320+-0.1+10.7", + 200, + "application/x-votable+xml", + None, + ), + ( + "POS=CIRCLE+320+-0.1+10.7&TIME=-Inf++Inf", + 200, + "application/x-votable+xml", + None, + ), + ( + "POS=CIRCLE+320+-0.1+10.7&TIME=-Inf++Inf&DP_TYPE=image", + 200, + "application/x-votable+xml", + None, + ), + ( + "pos=CIRCLE+320+-0.1+10.7&TIME=-Inf++Inf&DP_TYPE=image", + 200, + "application/x-votable+xml", + None, + ), + ( + "POS=RANGE+0+360.0+-2.0+2.0&TIME=-Inf++Inf&DP_TYPE=image&dp_type" + "=cube", + 200, + "application/x-votable+xml", + None, + ), + ], +) +async def test_query_endpoint_mocker_get( + client: AsyncClient, + query_params: str, + expected_status: int, + expected_content_type: str, + expected_message: str | None, + mock_siav2_query: MockButlerQueryService, + mock_butler: MockButler, + expected_votable: str, +) -> None: + """Test ``GET /api/sia/query`` with valid parameters but use a Mocker + for the Butler SIAv2 query. + """ + response = await client.get( + f"{config.path_prefix}/dp02/query?{query_params}", + ) + + # Remove XML declaration and comments + cleaned_response = re.sub( + r"<\?xml.*?\?>\s*", "", response.text, flags=re.DOTALL + ) + cleaned_response = re.sub( + r"\s*", "", cleaned_response, flags=re.DOTALL + ) + + assert cleaned_response == expected_votable + assert response.status_code == expected_status + assert response.headers["content-type"] == expected_content_type + assert "content-disposition" in response.headers + assert response.headers["content-disposition"].startswith( + f"attachment; filename={RESULT_NAME}.xml", + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ( + "query_params", + "expected_status", + "expected_content_type", + "expected_message", + ), + [ + ( + "POS=SOME_SHAPE+321+0+1&BAND=700e-9&FORMAT=votable", + 400, + "application/xml", + EXCEPTION_MESSAGES["invalid_pos"], + ), + ( + "POS=CIRCLE+0+0+1&TIME=ABC", + 400, + "application/xml", + EXCEPTION_MESSAGES["invalid_time"], + ), + ( + "POS=CIRCLE+0+0+1&CALIB=6", + 400, + "application/xml", + EXCEPTION_MESSAGES["invalid_calib"], + ), + ( + "MAXREC=0", + 200, + "application/x-votable+xml", + None, + ), + ], +) +async def test_query_endpoint_get( + client_direct: AsyncClient, + query_params: str, + expected_status: int, + expected_content_type: str, + expected_message: str | None, +) -> None: + response = await client_direct.get( + f"{config.path_prefix}/hsc/query?{query_params}" + ) + + assert response.status_code == expected_status + assert expected_content_type in response.headers["content-type"] + + if expected_status == 200: + assert "content-disposition" in response.headers + assert response.headers["content-disposition"].startswith( + f"attachment; filename={RESULT_NAME}.xml" + ) + elif expected_status == 400: + validate_votable_error(response, expected_message) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ( + "post_data", + "expected_status", + "expected_content_type", + "expected_message", + ), + [ + ( + { + "POS": "SOME_SHAPE 321 0 1", + "BAND": "700e-9", + "FORMAT": "votable", + }, + 400, + "application/xml", + EXCEPTION_MESSAGES["invalid_pos"], + ), + ( + {"pos": "CIRCLE 0 0 1", "TIME": "ABC"}, + 400, + "application/xml", + EXCEPTION_MESSAGES["invalid_time"], + ), + ( + {"POS": "CIRCLE 321 0 1", "BAND": "700e-9", "FORMAT": "votable"}, + 200, + "application/x-votable+xml", + EXCEPTION_MESSAGES["invalid_time"], + ), + ( + {"MAXREC": "0"}, + 200, + "application/x-votable+xml", + None, + ), + ], +) +async def test_query_endpoint_post( + client_direct: AsyncClient, + post_data: dict[str, Any], + expected_status: int, + expected_content_type: str, + expected_message: str | None, + expected_votable: str, +) -> None: + """Test ``POST /api/sia/query`` with various parameters.""" + response = await client_direct.post( + f"{config.path_prefix}/hsc/query", data=post_data + ) + assert response.status_code == expected_status + + if expected_status == 200: + assert "content-disposition" in response.headers + assert response.headers["content-disposition"].startswith( + f"attachment; filename={RESULT_NAME}.xml" + ) + elif expected_status == 400: + validate_votable_error(response, expected_message) diff --git a/tests/handlers/external_test.py b/tests/handlers/external_test.py deleted file mode 100644 index 734a816..0000000 --- a/tests/handlers/external_test.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tests for the vosiav2.handlers.external module and routes.""" - -from __future__ import annotations - -import pytest -from httpx import AsyncClient - -from vosiav2.config import config - - -@pytest.mark.asyncio -async def test_get_index(client: AsyncClient) -> None: - """Test ``GET /vo-siav2/``.""" - response = await client.get("/vo-siav2/") - assert response.status_code == 200 - data = response.json() - metadata = data["metadata"] - assert metadata["name"] == config.name - assert isinstance(metadata["version"], str) - assert isinstance(metadata["description"], str) - assert isinstance(metadata["repository_url"], str) - assert isinstance(metadata["documentation_url"], str) diff --git a/tests/handlers/internal_test.py b/tests/handlers/internal_test.py index 8ba931c..1c71694 100644 --- a/tests/handlers/internal_test.py +++ b/tests/handlers/internal_test.py @@ -1,11 +1,11 @@ -"""Tests for the vosiav2.handlers.internal module and routes.""" +"""Tests for the sia.handlers.internal module and routes.""" from __future__ import annotations import pytest from httpx import AsyncClient -from vosiav2.config import config +from sia.config import config @pytest.mark.asyncio diff --git a/tests/middeware/__init__.py b/tests/middeware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/middeware/ivoa_test.py b/tests/middeware/ivoa_test.py new file mode 100644 index 0000000..a8e3720 --- /dev/null +++ b/tests/middeware/ivoa_test.py @@ -0,0 +1,53 @@ +"""Tests for the IVOA middleware.""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_lowercase_form_keys(client: AsyncClient) -> None: + """Test that the form keys are converted to lowercase.""" + response = await client.post( + "/test-params", data={"UpperCase": "value", "MixedCase": "another"} + ) + assert response.status_code == 200 + assert response.json() == { + "method": "POST", + "form_data": {"uppercase": "value", "mixedcase": "another"}, + } + + +@pytest.mark.asyncio +async def test_preserve_value_case(client: AsyncClient) -> None: + """Test that the form values are preserved.""" + response = await client.post( + "/test-params", data={"key": "MixedCaseValue"} + ) + assert response.status_code == 200 + assert response.json() == { + "method": "POST", + "form_data": {"key": "MixedCaseValue"}, + } + + +@pytest.mark.asyncio +async def test_empty_form(client: AsyncClient) -> None: + """Test that an empty form is handled correctly.""" + response = await client.post("/test-params", data={}) + assert response.status_code == 200 + assert response.json() == {"method": "POST", "form_data": {}} + + +@pytest.mark.asyncio +async def test_content_type(client: AsyncClient) -> None: + """Test that the middleware handles different content types.""" + response = await client.post( + "/test-params", + data={"UpperCase": "value"}, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.status_code == 200 + assert response.json() == { + "method": "POST", + "form_data": {"uppercase": "value"}, + } diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/config_test.py b/tests/models/config_test.py new file mode 100644 index 0000000..1ea36bc --- /dev/null +++ b/tests/models/config_test.py @@ -0,0 +1,46 @@ +"""Test the Config model.""" + +import json + +import pytest +from pydantic_settings import SettingsError + +from sia.config import Config +from sia.exceptions import FatalFaultError +from sia.models.data_collections import ButlerDataCollection + + +@pytest.mark.asyncio +async def test_empty_config( + test_config_remote: Config, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that an empty Config raises a FatalFaultError.""" + monkeypatch.setenv("SIA_BUTLER_DATA_COLLECTIONS", "") + + with pytest.raises( + SettingsError, + match='error parsing value for field "butler_data_collections" ' + 'from source "EnvSettingsSource"', + ): + Config() + + +@pytest.mark.asyncio +async def test_config_no_butler_type( + test_config_remote: Config, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a Config with no default collection raises a + FatalFaultError. + """ + sample_collection: list[ButlerDataCollection] = [] + + monkeypatch.setenv( + "SIA_BUTLER_DATA_COLLECTIONS", json.dumps(sample_collection) + ) + + with pytest.raises( + FatalFaultError, + match="FatalFault: No Data Collections configured. " + "Please configure at least one Data collection.", + ): + Config() diff --git a/tests/models/sia_params_test.py b/tests/models/sia_params_test.py new file mode 100644 index 0000000..b4ec4cb --- /dev/null +++ b/tests/models/sia_params_test.py @@ -0,0 +1,160 @@ +"""Tests for cutout parameter models.""" + +from __future__ import annotations + +import pytest + +from sia.models.common import CaseInsensitiveEnum +from sia.models.sia_query_params import ( + CalibLevel, + DPType, + Polarization, + Shape, + SIAQueryParams, +) + + +@pytest.mark.asyncio +async def test_case_insensitive_enum() -> None: + """Test the CaseInsensitiveEnum class.""" + + class TestEnum(CaseInsensitiveEnum): + A = "a" + B = "b" + + assert TestEnum("a") == TestEnum.A + assert TestEnum("A") == TestEnum.A + assert TestEnum("b") == TestEnum.B + assert TestEnum("B") == TestEnum.B + + with pytest.raises(ValueError, match="'c' is not a valid TestEnum"): + TestEnum("c") + + +@pytest.mark.asyncio +async def test_shape_enum() -> None: + """Test the Shape enum.""" + assert Shape("circle") == Shape.CIRCLE + assert Shape("RANGE") == Shape.RANGE + assert Shape("Polygon") == Shape.POLYGON + + with pytest.raises(ValueError, match="'square' is not a valid Shape"): + Shape("square") + + +@pytest.mark.asyncio +async def test_dptype_enum() -> None: + """Test the DPType enum.""" + assert DPType("image") == DPType.IMAGE + assert DPType("CUBE") == DPType.CUBE + + with pytest.raises(ValueError, match="'video' is not a valid DPType"): + DPType("video") + + +@pytest.mark.asyncio +async def test_polarization_enum() -> None: + """Test the Polarization enum.""" + assert Polarization("i") == Polarization.I + assert Polarization("RR") == Polarization.RR + assert Polarization("xy") == Polarization.XY + + with pytest.raises(ValueError, match="'Z' is not a valid Polarization"): + Polarization("Z") + + +@pytest.mark.asyncio +async def test_sia_params_initialization() -> None: + """Test the initialization of SIAv2QueryParams.""" + params = SIAQueryParams( + pos=["CIRCLE 0 1 1"], + format=["application/fits"], + time=["55 55"], + band=["0.1 10.0"], + pol=[Polarization("I"), Polarization("Q")], + fov=["1.0 2.0"], + spatres=["0.1 0.2"], + exptime=["-Inf 60"], + timeres=["-Inf 1.0"], + specrp=["1000 2000"], + id=["obs_id_1"], + dptype=[DPType("image")], + calib=[CalibLevel(0), CalibLevel(1), CalibLevel(2)], + target=["M31"], + collection=["HST"], + facility=["HST"], + instrument=["ACS"], + maxrec=10, + responseformat="application/x-votable+xml", + ) + + assert params.pos == ["CIRCLE 0 1 1"] + assert params.format == ["application/fits"] + assert params.time == ["55 55"] + assert params.band == ["0.1 10.0"] + assert params.pol == [Polarization("I"), Polarization("Q")] + assert params.fov == ["1.0 2.0"] + assert params.spatres == ["0.1 0.2"] + assert params.exptime == ["-Inf 60"] + assert params.timeres == ["-Inf 1.0"] + assert params.specrp == ["1000 2000"] + assert params.id == ["obs_id_1"] + assert params.dptype == [DPType("image")] + assert params.calib == [0, 1, 2] + assert params.target == ["M31"] + assert params.collection == ["HST"] + assert params.facility == ["HST"] + assert params.instrument == ["ACS"] + assert params.maxrec == 10 + assert params.responseformat == "application/x-votable+xml" + + +@pytest.mark.asyncio +async def test_sia_params_default_values() -> None: + """Test the default values of SIAv2QueryParams.""" + params = SIAQueryParams() + + assert params.pos is None + assert params.format is None + assert params.time is None + assert params.band is None + assert params.pol is None + assert params.fov is None + assert params.spatres is None + assert params.exptime is None + assert params.timeres is None + assert params.specrp is None + assert params.id is None + assert params.dptype is None + assert params.calib is None + assert params.target is None + assert params.collection is None + assert params.facility is None + assert params.instrument is None + assert params.maxrec == 0 + assert params.responseformat is None + + +@pytest.fixture +def sample_sia_params() -> SIAQueryParams: + return SIAQueryParams( + pos=["CIRCLE 0 1 1 1"], + format=["application/fits"], + time=["55"], + band=["0.1 10.0"], + pol=[Polarization("I"), Polarization("Q")], + fov=["1.0 2.0"], + spatres=["0.1 0.2"], + exptime=["-Inf 60"], + timeres=["-Inf 1.0"], + specrp=["1000 2000"], + id=["obs_id_1"], + dptype=[DPType("image")], + calib=[CalibLevel(0), CalibLevel(1), CalibLevel(2)], + target=["M31"], + collection=["HST"], + facility=["HST"], + instrument=["ACS"], + maxrec=10, + responseformat="application/x-votable+xml", + ) diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/services/data_collection_test.py b/tests/services/data_collection_test.py new file mode 100644 index 0000000..d22c976 --- /dev/null +++ b/tests/services/data_collection_test.py @@ -0,0 +1,107 @@ +"""Tests for the config_reader module.""" + +from pathlib import Path + +import pytest +from pydantic import HttpUrl + +from sia.config import Config +from sia.exceptions import FatalFaultError +from sia.services.data_collections import DataCollectionService + +BASE_PATH = Path(__file__).parent + + +@pytest.mark.asyncio +async def test_get_data_repositories(test_config_remote: Config) -> None: + """Test get_data_repositories function.""" + expected_repos = { + "LSST.DP02": "https://example.com/api/butler/repo/dp02/butler.yaml" + } + + result = DataCollectionService( + config=test_config_remote + ).get_data_repositories() + + assert ( + result == expected_repos + ), f"Expected {expected_repos}, but got {result}" + assert len(result) == 1, f"Expected 1 repository, but got {len(result)}" + assert "LSST.DP02" in result, "Expected 'LSST.DP02' to be in the result" + assert result["LSST.DP02"] == ( + "https://example.com/api/butler/repo/dp02/butler.yaml" + ), f"Unexpected repository URL for LSST.DP02: {result['LSST.DP02']}" + + +@pytest.mark.asyncio +async def test_get_data_collection_with_label( + test_config_remote: Config, +) -> None: + """Test get_data_collection function with a label.""" + label = "LSST.DP02" + result = DataCollectionService( + config=test_config_remote + ).get_data_collection_by_label(label=label) + assert result.label == label + assert result.repository == HttpUrl( + "https://example.com/api/butler/repo/dp02/butler.yaml" + ) + + +@pytest.mark.asyncio +async def test_get_data_collection_with_name( + test_config_remote: Config, +) -> None: + """Test get_data_collection function with a name.""" + name = "dp02" + result = DataCollectionService( + config=test_config_remote + ).get_data_collection_by_name(name=name) + assert result.name == name + assert result.repository == HttpUrl( + "https://example.com/api/butler/repo/dp02/butler.yaml" + ) + + +@pytest.mark.asyncio +async def test_get_data_collection_no_label( + test_config_remote: Config, +) -> None: + """Test get_data_collection function with no label.""" + with pytest.raises( + ValueError, + match="Label is required.", + ): + DataCollectionService( + config=test_config_remote + ).get_data_collection_by_label(label="") + + +@pytest.mark.asyncio +async def test_get_data_collection_empty_config() -> None: + """Test get_data_collection function with an empty configuration.""" + with pytest.raises( + FatalFaultError, + match="FatalFault: No Data Collections configured. Please configure " + "at least one Data collection.", + ): + empty_config = Config( + butler_data_collections=[], + ) + DataCollectionService( + config=empty_config + ).get_data_collection_by_label(label="") + + +@pytest.mark.asyncio +async def test_get_data_collection_invalid_label( + test_config_remote: Config, +) -> None: + """Test get_data_collection function with an invalid label.""" + with pytest.raises( + KeyError, + match="Label InvalidLabel not found in Data collections", + ): + DataCollectionService( + config=test_config_remote + ).get_data_collection_by_label(label="InvalidLabel") diff --git a/tests/support/__init__.py b/tests/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/support/butler.py b/tests/support/butler.py new file mode 100644 index 0000000..43ba6c2 --- /dev/null +++ b/tests/support/butler.py @@ -0,0 +1,137 @@ +"""Support module for mockign a Butler.""" + +from collections.abc import Iterator +from typing import Any +from unittest.mock import Mock, patch +from uuid import UUID, uuid4 + +import astropy +from lsst.daf.butler import Butler, LabeledButlerFactory +from lsst.resources import ResourcePath + +__all__ = ["MockButlerQueryService", "patch_siav2_query"] + + +class MockButlerQueryService: + @staticmethod + def siav2_query() -> astropy.io.votable.tree.VOTableFile: + """Create a mock ObsCore VOTable. + + Returns + ------- + astropy.io.votable.tree.VOTableFile + The mock ObsCore VOTable. + """ + from astropy.io.votable import from_table + from astropy.table import Table + + # Create an Astropy Table with Obscore columns + t = Table( + names=( + "dataproduct_type", + "s_ra", + "s_dec", + "s_fov", + "t_min", + "t_max", + "em_min", + "em_max", + "o_ucd", + "access_url", + "access_format", + "obs_publisher_did", + ), + dtype=( + "str", + "float", + "float", + "float", + "str", + "str", + "float", + "float", + "str", + "str", + "str", + "str", + ), + ) + + t.add_row( + ( + "image", + 180.0, + -30.0, + 0.1, + "2020-01-01", + "2020-01-02", + 4.0e-7, + 7.0e-7, + "phot.flux", + "http://example.com/image.fits", + "application/fits", + "ivo://example/123", + ) + ) + + return from_table(t) + + +__all__ = ["MockButler", "patch_butler"] + + +class MockDatasetRef: + """Mock of a Butler DatasetRef.""" + + def __init__(self, uuid: UUID, dataset_type: str) -> None: + self.uuid = uuid + self.datasetType = self + self.name = dataset_type + + +class MockButler(Mock): + """Mock of Butler for testing.""" + + def __init__(self) -> None: + super().__init__(spec=Butler) + self.uuid = uuid4() + self.is_raw = False + self.mock_uri: str | None = None + + def _generate_mock_uri(self, ref: MockDatasetRef) -> str: + if self.mock_uri is None: + return f"s3://some-bucket/{ref.uuid!s}" + return self.mock_uri + + def _get_child_mock(self, /, **kwargs: Any) -> Mock: + return Mock(**kwargs) + + def get_dataset(self, uuid: UUID) -> MockDatasetRef | None: + dataset_type = "raw" if self.is_raw else "calexp" + if uuid == self.uuid: + return MockDatasetRef(uuid, dataset_type) + else: + return None + + def getURI(self, ref: MockDatasetRef) -> ResourcePath: # noqa: N802 + resource_path = ResourcePath(self._generate_mock_uri(ref)) + # 'size' does I/O, so mock it out + mock = patch.object(resource_path, "size").start() + mock.return_value = 1234 + return resource_path + + +def patch_butler() -> Iterator[MockButler]: + """Mock out Butler for testing.""" + mock_butler = MockButler() + with patch.object(LabeledButlerFactory, "create_butler") as mock: + mock.return_value = mock_butler + yield mock_butler + + +def patch_siav2_query() -> Iterator[MockButlerQueryService]: + """Mock out Butler siav2_query for testing.""" + mock_siav2_query = MockButlerQueryService() + with patch("sia.handlers.external.siav2_query") as mock: + mock.return_value = mock_siav2_query.siav2_query() + yield mock_siav2_query diff --git a/tests/support/constants.py b/tests/support/constants.py new file mode 100644 index 0000000..dd1886c --- /dev/null +++ b/tests/support/constants.py @@ -0,0 +1,9 @@ +"""Constants for the tests.""" + +EXCEPTION_MESSAGES = { + "invalid_pos": "UsageFault: Unrecognized shape in POS string", + "invalid_calib": "UsageFault: [{'type': 'enum', 'loc': " + "('query', 'calib', 0), 'msg': 'Input should be 0, 1, 2 or 3', " + "'input': '6', 'ctx': {'expected': '0, 1, 2 or 3'}}]", + "invalid_time": "UsageFault: could not convert string to float:", +} diff --git a/tests/support/validators.py b/tests/support/validators.py new file mode 100644 index 0000000..34ceca7 --- /dev/null +++ b/tests/support/validators.py @@ -0,0 +1,113 @@ +"""Validators for testing the responses of the API.""" + +from defusedxml import ElementTree as DefusedET +from httpx import Response + + +def validate_response( + response: Response, status_code: int, content_type: str +) -> None: + """Validate that the response is an error with the expected status code + and content_type. + + Parameters + ---------- + response + The response to validate. + status_code + The expected status code. + content_type + The expected error message. + + Raises + ------ + AssertionError + If the response is not an expected error + """ + assert response.status_code == status_code + assert content_type in response.headers["Content-Type"] + + +def validate_votable_error( + response: Response, + expected_message: str | None, +) -> None: + """Validate that the response is a VOTable error with the expected message. + + Parameters + ---------- + response + The response to validate. + expected_message + The expected error message. + + Raises + ------ + AssertionError + If the response is not a VOTable error with the expected message. + """ + root = DefusedET.fromstring(response.text) + assert root.tag == "{http://www.ivoa.net/xml/VOTable/v1.3}VOTABLE" + assert root.attrib["version"] == "1.3" + resource = root.find("{http://www.ivoa.net/xml/VOTable/v1.3}RESOURCE") + assert resource is not None + info = resource.find("{http://www.ivoa.net/xml/VOTable/v1.3}INFO") + assert info is not None + assert info.attrib["name"] == "QUERY_STATUS" + assert info.attrib["value"] == "ERROR" + if expected_message and info.text: + assert info.text.startswith(expected_message) + + +def valid_votable(xml_string: str) -> bool: + """Check if the VOTable structure is valid and has at least one Table row. + + Parameters + ---------- + xml_string + The VOTable XML string to validate. + + Returns + ------- + bool + True if the XML string is a valid VOTable with some data, + False otherwise. + """ + try: + root = DefusedET.fromstring(xml_string) + resource = root.find("{http://www.ivoa.net/xml/VOTable/v1.3}RESOURCE") + if resource is None: + return False + + info = resource.find("{http://www.ivoa.net/xml/VOTable/v1.3}INFO") + if info is not None: + if ( + info is None + or info.get("name") != "QUERY_STATUS" + or info.get("value") != "OK" + ): + return False + + table = resource.find("{http://www.ivoa.net/xml/VOTable/v1.3}TABLE") + + if table is None: + return False + + data = table.find("{http://www.ivoa.net/xml/VOTable/v1.3}DATA") + if data is None: + return False + + tabledata = data.find( + "{http://www.ivoa.net/xml/VOTable/v1.3}TABLEDATA" + ) + + if tabledata is None: + return False + + return ( + tabledata.find("{http://www.ivoa.net/xml/VOTable/v1.3}TR") + is not None + ) + + except DefusedET.ParseError: + return False diff --git a/tests/templates/availability.xml b/tests/templates/availability.xml new file mode 100644 index 0000000..af2dc46 --- /dev/null +++ b/tests/templates/availability.xml @@ -0,0 +1 @@ +true diff --git a/tests/templates/capabilities.xml b/tests/templates/capabilities.xml new file mode 100644 index 0000000..5a4a3ca --- /dev/null +++ b/tests/templates/capabilities.xml @@ -0,0 +1,25 @@ + + + + + {{ capabilities_url }} + + + + + {{ availability_url }} + + + + + + + + + + {{ query_url }} + + + \ No newline at end of file diff --git a/tests/templates/expected_votable.xml b/tests/templates/expected_votable.xml new file mode 100644 index 0000000..949a4d5 --- /dev/null +++ b/tests/templates/expected_votable.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + image + 180 + -30 + 0.1 + 2020-01-01 + 2020-01-02 + 4e-07 + 7e-07 + phot.flux + http://example.com/image.fits + application/fits + ivo://example/123 + + + + + + diff --git a/tox.ini b/tox.ini index ebbbc2d..54f8332 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,15 @@ deps = -r{toxinidir}/requirements/main.txt -r{toxinidir}/requirements/dev.txt commands = - pytest --cov=vosiav2 --cov-branch --cov-report= {posargs} + pytest --cov=sia --cov-branch --cov-report= {posargs} + +[testenv:py] +setenv = + SIA_BUTLER_DATA_COLLECTIONS=[{"config":"https://example.com/dp02.yaml", \ + "datalink_url":"https://example.com/links?ID=butler%3A//dp02", \ + "name":"dp02", "label":"LSST.DP02", "butler_type":"REMOTE", \ + "default_instrument": "LSSTCam-imSim", \ + "repository":"https://example.com/repo/dp02/butler.yaml"}] [testenv:coverage-report] description = Compile coverage from each test run. @@ -21,7 +29,7 @@ commands = coverage report [testenv:typing] description = Run mypy. commands = - mypy src/vosiav2 tests + mypy src/sia tests [testenv:lint] description = Lint codebase by running pre-commit @@ -33,4 +41,4 @@ commands = pre-commit run --all-files [testenv:run] description = Run the development server with auto-reload for code changes. usedevelop = true -commands = uvicorn vosiav2.main:app --reload +commands = uvicorn sia.main:app --reload