diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a5979b5..33b02c1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: check-toml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.10 + rev: v0.5.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/adamchainz/blacken-docs - rev: 1.16.0 + rev: 1.18.0 hooks: - id: blacken-docs additional_dependencies: [black==24.3.0] diff --git a/changelog.d/20240711_095604_danfuchs_nicer_github_integration_config.md b/changelog.d/20240711_095604_danfuchs_nicer_github_integration_config.md new file mode 100644 index 00000000..48f83d0a --- /dev/null +++ b/changelog.d/20240711_095604_danfuchs_nicer_github_integration_config.md @@ -0,0 +1,6 @@ + + +### Backwards-incompatible changes + +- GitHub CI and refresh app config are now each a separate, all-or-nothing set of config that comes from a mix of a yaml file and env vars. This requires some new and different Helm values in Phalanx (see https://mobu.lsst.io/operations/github_ci_app.html#add-phalanx-configuration) +- The GitHub CI app now takes the scopes it assigns from config values, rather than hardcoding a list of scopes. diff --git a/docs/operations/github_ci_app.rst b/docs/operations/github_ci_app.rst new file mode 100644 index 00000000..e368cd56 --- /dev/null +++ b/docs/operations/github_ci_app.rst @@ -0,0 +1,72 @@ +###################################### +Adding a new GitHub CI app integration +###################################### + + +Create a new GitHub app +======================= + +#. Click the ``New GitHub App`` button in the `lsst-sqre org Developer Settings apps page `__. +#. Name it :samp:`mobu CI ({env URL or id if the URL is too long})`. +#. Make sure the ``Active`` checkbox is checked in the ``Webhook`` section. +#. Enter :samp:`https://{env URL}/mobu/github/ci/webhook` in the :guilabel:`Webhook URL` input. +#. Generate a strong password to use as the webhook secret. +#. Store this in the ``SQuaRE`` vault in the ``LSST IT`` 1Password account in an ``Server`` item named :samp:`mobu ({env URL})` in a ``password`` field named ``mobu-github-ci-app-webhook-secret``. +#. Get this into the Phalanx secret store for that env under the key: ``github-ci-app-webhook-secret`` (`this process `__ is different for different envs). +#. Enter this secret in the :guilabel:`Webhook secret (optional)` box in the GitHub App config. +#. Select :menuselection:`Read and Write` in the dropdown of the :guilabel:`Checks` access category in the :guilabel:`Repository Permissions` section. +#. Select :menuselection:`Read-only` in the dropdown of the :guilabel:`Contents` access category in the :guilabel:`Repository Permissions` section. +#. Check the :guilabel:`Check suite` and :guilabel:`Check run` checkboxes in the :guilabel:`Subscribe to events` section. +#. Select the :guilabel:`Any account` radio button in the :guilabel:`Where can this GitHub App be installed?` section. +#. Click the :guilabel:`Create GitHub App` button. +#. Find the :guilabel:`App ID` (an integer) in the :guilabel:`About` section. Get this into the Phalanx secret store for that env under the key: ``github-ci-app-id`` (`this process `__ is different for different envs). +#. Click the :guilabel:`Generate a private key` button in the :guilabel:`Private keys` section. +#. Store this private key in the same :samp:`mobu ({env URL})` item in a ``text`` key called ``github-mobu-ci-app-private-key``. +#. Get this into the Phalanx secret store for that env under the key: ``github-ci-app-private-key`` (`this process `__ is different for different envs). + +Install the app for a repo +========================== + +#. Go to new app’s homepage (something like https://github.com/apps/mobu-refresh-usdfdev). +#. Click the :guilabel:`Install` button. +#. Select the :guilabel:`Only select repositories` radio button. +#. Select the repo in the dropdown. +#. Click :guilabel:`Install`. + +Add Phalanx configuration +========================= +In :samp:`applications/mobu/values-{env}.yaml`, add a ``config.githubCiApp`` value: + +.. code:: yaml + + config: + github: + acceptedGithubOrgs: + - lsst-sqre + users: + - username: "bot-mobu-ci-user-1" + uidnumber: 123 + gidnumber: 456 + - username: "bot-mobu-ci-user-2" + uidnumber: 789 + gidnumber: 876 + scopes: + - "exec:notebook" + - "exec:portal" + - "read:image" + - "read:tap" + +All items are required. + +``accepted_github_orgs`` + A list of GitHub organizations from which this instance of Mobu will accept webhook requests. + Webhook requests from any orgs not in this list will get a ``403`` response. + +``users`` + Follows the same rules as the ``users`` list in a flock autostart config. + The usernames must all start with ``bot-mobu``. + In envs with Firestore integration, you only need to specify ``username``. + In envs without it, you need to ensure that users are manually provisioned, and then you need all three of ``username``, ``uidnumber``, and ``gidnumber``. + +``scopes`` + A list of `Gafaelfawr scopes `__ to grant to the users running in the monkeys started from GitHub CI checks. diff --git a/docs/operations/github_refresh_app.rst b/docs/operations/github_refresh_app.rst new file mode 100644 index 00000000..01e868ff --- /dev/null +++ b/docs/operations/github_refresh_app.rst @@ -0,0 +1,49 @@ +########################################### +Adding a new GitHub Refresh app integration +########################################### + +Adding the GitHub refresh app integration to a new environment requires configuring things in GitHub and Phalanx. + +Create a new GitHub app +======================= + + +#. Click the ``New GitHub App`` button in the `lsst-sqre org Developer Settings apps page `__. +#. Name it :samp:`mobu refresh ({env URL or id if the URL is too long})`. +#. Make sure the :guilabel:`Active` checkbox is checked in the :guilabel:`Webhook` section. +#. Enter :samp:`https://{env URL}/mobu/github/refresh/webhook` in the :guilabel:`Webhook URL` input. +#. Generate a strong password to use as the webhook secret. +#. Store this in the ``SQuaRE`` vault in the ``LSST IT`` 1Password account in an ``Server`` item named :samp:`mobu ({env URL})` in a ``password`` field called ``github-refresh-app-webhook-secret``. +#. Get this into the Phalanx secret store for that env under the key: ``github-refresh-app-webhook-secret`` (`this process `__ is different for different envs). +#. Enter this secret in the :guilabel:`Webhook secret (optional)` box in the GitHub App config. +#. Select :menuselection:`Read and Write` in the dropdown of the :guilabel:`Checks` access category in the :guilabel:`Repository Permissions` section. +#. Select :menuselection:`Read-only` in the dropdown of the :guilabel:`Contents` access category in the :guilabel:`Repository Permissions` section. +#. Check the :guilabel:`Check suite` and :guilabel:`Check run` checkboxes in the :guilabel:`Subscribe to events` section. +#. Select the :guilabel:`Any account` radio button in the :guilabel:`Where can this GitHub App be installed?` section. +#. Click the :guilabel:`Create GitHub App` button. + +Install the app for a repo +========================== + +#. Go to new app’s homepage (something like https://github.com/apps/mobu-refresh-usdfdev). +#. Click the :guilabel:`Install` button. +#. Select the :guilabel:`Only select repositories` radio button. +#. Select the repo in the dropdown. +#. Click :guilabel:`Install`. + +Add Phalanx configuration +========================= +In :samp:`applications/mobu/values-{env}.yaml`, add a ``config.githubRefreshApp`` value: + +.. code:: yaml + + config: + githubRefreshApp: + acceptedGithubOrgs: + - lsst-sqre + +All of these items are required. + +``accepted_github_orgs`` + A list of GitHub organizations from which this instance of Mobu will accept webhook requests. + Webhook requests from any orgs not in this list will get a ``403`` response. diff --git a/docs/operations/index.rst b/docs/operations/index.rst index 47f17c74..7be148c6 100644 --- a/docs/operations/index.rst +++ b/docs/operations/index.rst @@ -7,114 +7,13 @@ GitHub integration Each integration has as GitHub application created in the `lsst-sqre org `__ for every environment in which it is enabled. -All of the applications: +All of the GitHub applications: * `mobu refresh (data-dev.lsst.cloud) `__ * `mobu CI (data-dev.lsst.cloud) `__ -GitHub application configuration -================================ +.. toctree:: + :maxdepth: 1 -To enable the GitHub integrations for another mobu env, you have to create a new GitHub application and sync Phalanx secrets. - -Refresh app ------------ - -Create a new GitHub app -~~~~~~~~~~~~~~~~~~~~~~~ - - -#. Click the ``New GitHub App`` button in the `lsst-sqre org Developer Settings apps page `__. - -#. Name it ``mobu refresh ()``. - -#. Make sure the ``Active`` checkbox is checked in the ``Webhook`` section. - -#. Enter ``https:///mobu/github/refresh/webhook`` in the ``Webhook URL`` input. -#. Generate a strong password to use as the webhook secret. -#. Store this in the ``SQuaRE`` vault in the ``LSST IT`` 1Password account in an item named ``mobu GitHub refresh app webhook secret ()``. -#. Get this into the Phalanx secret store for that env under the key: ``github-refresh-app-webhook-secret`` (`this process `__ is different for different envs). -#. Enter this secret in the ``Webhook secret (optional)`` box in the GitHub App config. -#. Select ``Read-only`` in the dropdown of the ``Contents`` access category in the ``Repository Permissions`` section. -#. Check the ``Push`` checkbox in the ``Subscribe to events`` section. -#. Select the ``Any account`` radio button in the ``Where can this GitHub App be installed?`` section. -#. Click the ``Create GitHub App`` button. -#. Do the `Phalanx configuration <#phalanx-configuration>`__. - -Install the app for a repo -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#. Go to new app’s homepage (something like https://github.com/apps/mobu-refresh-usdfdev). -#. Click the ``Install`` button. -#. Select the ``Only select repositories`` radio button. -#. Select the repo in the dropdown. -#. Click ``Install``. - -CI app ------- - -Create a new GitHub app -~~~~~~~~~~~~~~~~~~~~~~~ - -#. Click the ``New GitHub App`` button in the `lsst-sqre org Developer Settings apps page `__. -#. Name it ``mobu CI ()``. -#. Make sure the ``Active`` checkbox is checked in the ``Webhook`` section. -#. Enter ``https:///mobu/github/ci/webhook`` in the ``Webhook URL`` input. -#. Generate a strong password to use as the webhook secret. -#. Store this in the ``SQuaRE`` vault in the ``LSST IT`` 1Password account in an item named ``mobu GitHub CI app webhook secret ()``. -#. Get this into the Phalanx secret store for that env under the key: ``github-ci-app-webhook-secret`` (`this process `__ is different for different envs). -#. Enter this secret in the ``Webhook secret (optional)`` box in the GitHub App config. -#. Select ``Read and Write`` in the dropdown of the ``Checks`` access category in the ``Repository Permissions`` section. -#. Select ``Read-only`` in the dropdown of the ``Contents`` access category in the ``Repository Permissions`` section. -#. Check the ``Check suite`` and ``Check run`` checkboxes in the ``Subscribe to events`` section. -#. Select the ``Any account`` radio button in the ``Where can this GitHub App be installed?`` section. -#. Click the ``Create GitHub App`` button. -#. Find the ``App ID`` (an integer) in the ``About`` section. Get this into the Phalanx secret store for that env under the key: ``github-ci-app-id`` (`this process `__ is different for different envs). -#. Click the ``Generate a private key`` button in the ``Private keys`` section. -#. Store this private key in the ``SQuaRE`` vault in the ``LSST IT`` 1Password account in an item named ``mobu GitHub CI app private key ()``. -#. Get this into the Phalanx secret store for that env under the key: ``github-ci-app-private-key`` (`this process `__ is different for different envs). -#. Do the `Phalanx configuration <#phalanx-configuration>`__. - -Install the app for a repo -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#. Go to new app’s homepage (something like https://github.com/apps/mobu-refresh-usdfdev). -#. Click the ``Install`` button. -#. Select the ``Only select repositories`` radio button. -#. Select the repo in the dropdown. -#. Click ``Install``. - -Phalanx configuration -===================== - -The GitHub integrations each need to be explicitly enabled in Phalanx for a given environment. -If an integration is not enabled, then the webhook route for that integration will not be mounted, GitHub webhook requests will get ``404`` responses. -To enable these integrations for an environment, set these values to ``true``: - -* ``config.githubRefreshAppEnabled`` -* ``config.githubCiAppEnabled`` - -If you want to enable either GitHub integration in a given environment, you also need to add a ``config.github`` section to that env’s values in Mobu. -That needs to be a dict with at ``users`` and ``accepted_github_orgs`` entries. -It should look something like this: - -.. code:: yaml - - config: - github: - accepted_github_orgs: - - lsst-sqre - users: - - username: "bot-mobu-ci-user-1" - uidnumber: 123 - gidnumber: 456 - - username: "bot-mobu-ci-user-2" - uidnumber: 789 - gidnumber: 876 - -The organization of any repo that uses any of the GitHub integrations in an env must be added to the ``accepted_github_orgs`` list, otherwise Github webhook requests will get ``403`` responses. - -The ``users`` list follows the same rules as the ``users`` list in a flock autostart config. -The usernames must all start with ``bot-mobu``. -In envs with Firestore integration, you only need to specify ``username``. -In envs without it, you need to ensure that users are manually provisioned, and then you need all three of ``username``, ``uidnumber``, and ``gidnumber``. + github_ci_app + github_refresh_app diff --git a/pyproject.toml b/pyproject.toml index b18e3dde..39936d03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,6 +165,7 @@ ignore = [ "TRY003", # good general advice but lint is way too aggressive "TRY301", # sometimes raising exceptions inside try is the best flow "UP040", # Python 3.12 supports `type` alias kw, but mypy doesn't yet + "S113", # httpx enforces timeouts everywhere, 5s by default # The following settings should be disabled when using ruff format # per https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules diff --git a/requirements/dev.txt b/requirements/dev.txt index 2e7f1e18..0e249233 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -40,9 +40,9 @@ beautifulsoup4==4.12.3 \ --hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \ --hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed # via pydata-sphinx-theme -certifi==2024.6.2 \ - --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ - --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 +certifi==2024.7.4 \ + --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ + --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 # via # -c requirements/main.txt # httpcore @@ -365,9 +365,9 @@ httpx==0.27.0 \ # via # -c requirements/main.txt # respx -identify==2.5.36 \ - --hash=sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa \ - --hash=sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d +identify==2.6.0 \ + --hash=sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf \ + --hash=sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0 # via pre-commit idna==3.7 \ --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ @@ -391,13 +391,13 @@ iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # via pytest -ipykernel==6.29.4 \ - --hash=sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da \ - --hash=sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c +ipykernel==6.29.5 \ + --hash=sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5 \ + --hash=sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215 # via myst-nb -ipython==8.25.0 \ - --hash=sha256:53eee7ad44df903a06655871cbab66d156a051fd86f3ec6750470ac9604ac1ab \ - --hash=sha256:c6ed726a140b6e725b911528f80439c534fac915246af3efc39440a6b0f9d716 +ipython==8.26.0 \ + --hash=sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c \ + --hash=sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff # via # ipykernel # myst-nb @@ -415,9 +415,9 @@ jinja2==3.1.4 \ # sphinx # sphinx-jinja # sphinxcontrib-redoc -jsonschema==4.22.0 \ - --hash=sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7 \ - --hash=sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802 +jsonschema==4.23.0 \ + --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ + --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 # via # nbformat # sphinxcontrib-redoc @@ -573,9 +573,9 @@ mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 # via mypy -myst-nb==1.1.0 \ - --hash=sha256:0ac29b2a346f9a1257edbfb5d6c47d528728a37e6b9438903c2821f69fda9235 \ - --hash=sha256:9278840e844f5d780b5acc5400cbf63d97caaccf8eb442a55ebd9a03e2522d5e +myst-nb==1.1.1 \ + --hash=sha256:74227c11f76d03494f43b7788659b161b94f4dedef230a2912412bc8c3c9e553 \ + --hash=sha256:8b8f9085287d948eef46cb3764aafc21915e0e981882b8c742719f5b1a84c36f # via documenteer myst-parser==3.0.1 \ --hash=sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1 \ @@ -677,92 +677,102 @@ pybtex-docutils==1.0.3 \ --hash=sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b \ --hash=sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9 # via sphinxcontrib-bibtex -pydantic==2.7.4 \ - --hash=sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52 \ - --hash=sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0 +pydantic==2.8.2 \ + --hash=sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a \ + --hash=sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8 # via # -c requirements/main.txt # documenteer -pydantic-core==2.18.4 \ - --hash=sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3 \ - --hash=sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8 \ - --hash=sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8 \ - --hash=sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30 \ - --hash=sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a \ - --hash=sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8 \ - --hash=sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d \ - --hash=sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc \ - --hash=sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2 \ - --hash=sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab \ - --hash=sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077 \ - --hash=sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e \ - --hash=sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9 \ - --hash=sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9 \ - --hash=sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef \ - --hash=sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1 \ - --hash=sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507 \ - --hash=sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528 \ - --hash=sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558 \ - --hash=sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b \ - --hash=sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154 \ - --hash=sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724 \ - --hash=sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695 \ - --hash=sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9 \ - --hash=sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851 \ - --hash=sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805 \ - --hash=sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a \ - --hash=sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5 \ - --hash=sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94 \ - --hash=sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c \ - --hash=sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d \ - --hash=sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef \ - --hash=sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26 \ - --hash=sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2 \ - --hash=sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c \ - --hash=sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0 \ - --hash=sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2 \ - --hash=sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4 \ - --hash=sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d \ - --hash=sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2 \ - --hash=sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce \ - --hash=sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34 \ - --hash=sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f \ - --hash=sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d \ - --hash=sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b \ - --hash=sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07 \ - --hash=sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312 \ - --hash=sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057 \ - --hash=sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d \ - --hash=sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af \ - --hash=sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb \ - --hash=sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd \ - --hash=sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78 \ - --hash=sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b \ - --hash=sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223 \ - --hash=sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a \ - --hash=sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4 \ - --hash=sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5 \ - --hash=sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23 \ - --hash=sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a \ - --hash=sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4 \ - --hash=sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8 \ - --hash=sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d \ - --hash=sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443 \ - --hash=sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e \ - --hash=sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f \ - --hash=sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e \ - --hash=sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d \ - --hash=sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc \ - --hash=sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443 \ - --hash=sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be \ - --hash=sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2 \ - --hash=sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee \ - --hash=sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f \ - --hash=sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae \ - --hash=sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864 \ - --hash=sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4 \ - --hash=sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951 \ - --hash=sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc +pydantic-core==2.20.1 \ + --hash=sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d \ + --hash=sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f \ + --hash=sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686 \ + --hash=sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482 \ + --hash=sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006 \ + --hash=sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83 \ + --hash=sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6 \ + --hash=sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88 \ + --hash=sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86 \ + --hash=sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a \ + --hash=sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6 \ + --hash=sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a \ + --hash=sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6 \ + --hash=sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6 \ + --hash=sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43 \ + --hash=sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c \ + --hash=sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4 \ + --hash=sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e \ + --hash=sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203 \ + --hash=sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd \ + --hash=sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1 \ + --hash=sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24 \ + --hash=sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc \ + --hash=sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc \ + --hash=sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3 \ + --hash=sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598 \ + --hash=sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98 \ + --hash=sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331 \ + --hash=sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2 \ + --hash=sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a \ + --hash=sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6 \ + --hash=sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688 \ + --hash=sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91 \ + --hash=sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa \ + --hash=sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b \ + --hash=sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0 \ + --hash=sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840 \ + --hash=sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c \ + --hash=sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd \ + --hash=sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3 \ + --hash=sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231 \ + --hash=sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1 \ + --hash=sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953 \ + --hash=sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250 \ + --hash=sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a \ + --hash=sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2 \ + --hash=sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20 \ + --hash=sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434 \ + --hash=sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab \ + --hash=sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703 \ + --hash=sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a \ + --hash=sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2 \ + --hash=sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac \ + --hash=sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611 \ + --hash=sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121 \ + --hash=sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e \ + --hash=sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b \ + --hash=sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09 \ + --hash=sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906 \ + --hash=sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9 \ + --hash=sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7 \ + --hash=sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b \ + --hash=sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987 \ + --hash=sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c \ + --hash=sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b \ + --hash=sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e \ + --hash=sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237 \ + --hash=sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1 \ + --hash=sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19 \ + --hash=sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b \ + --hash=sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad \ + --hash=sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0 \ + --hash=sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94 \ + --hash=sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312 \ + --hash=sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f \ + --hash=sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669 \ + --hash=sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1 \ + --hash=sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe \ + --hash=sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99 \ + --hash=sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a \ + --hash=sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a \ + --hash=sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52 \ + --hash=sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c \ + --hash=sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad \ + --hash=sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1 \ + --hash=sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a \ + --hash=sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f \ + --hash=sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a \ + --hash=sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27 # via # -c requirements/main.txt # pydantic @@ -983,135 +993,136 @@ respx==0.21.1 \ --hash=sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20 \ --hash=sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af # via -r requirements/dev.in -rpds-py==0.18.1 \ - --hash=sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee \ - --hash=sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc \ - --hash=sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc \ - --hash=sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944 \ - --hash=sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20 \ - --hash=sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7 \ - --hash=sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4 \ - --hash=sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6 \ - --hash=sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6 \ - --hash=sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93 \ - --hash=sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633 \ - --hash=sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0 \ - --hash=sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360 \ - --hash=sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8 \ - --hash=sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139 \ - --hash=sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7 \ - --hash=sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a \ - --hash=sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9 \ - --hash=sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26 \ - --hash=sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724 \ - --hash=sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72 \ - --hash=sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b \ - --hash=sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09 \ - --hash=sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100 \ - --hash=sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3 \ - --hash=sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261 \ - --hash=sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3 \ - --hash=sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9 \ - --hash=sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b \ - --hash=sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3 \ - --hash=sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de \ - --hash=sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d \ - --hash=sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e \ - --hash=sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8 \ - --hash=sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff \ - --hash=sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5 \ - --hash=sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c \ - --hash=sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e \ - --hash=sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e \ - --hash=sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4 \ - --hash=sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8 \ - --hash=sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922 \ - --hash=sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338 \ - --hash=sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d \ - --hash=sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8 \ - --hash=sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2 \ - --hash=sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72 \ - --hash=sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80 \ - --hash=sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644 \ - --hash=sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae \ - --hash=sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163 \ - --hash=sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104 \ - --hash=sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d \ - --hash=sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60 \ - --hash=sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a \ - --hash=sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d \ - --hash=sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07 \ - --hash=sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49 \ - --hash=sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10 \ - --hash=sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f \ - --hash=sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2 \ - --hash=sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8 \ - --hash=sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7 \ - --hash=sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88 \ - --hash=sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65 \ - --hash=sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0 \ - --hash=sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909 \ - --hash=sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8 \ - --hash=sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c \ - --hash=sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184 \ - --hash=sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397 \ - --hash=sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a \ - --hash=sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346 \ - --hash=sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590 \ - --hash=sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333 \ - --hash=sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb \ - --hash=sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74 \ - --hash=sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e \ - --hash=sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d \ - --hash=sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa \ - --hash=sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f \ - --hash=sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53 \ - --hash=sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1 \ - --hash=sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac \ - --hash=sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0 \ - --hash=sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd \ - --hash=sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611 \ - --hash=sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f \ - --hash=sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c \ - --hash=sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5 \ - --hash=sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab \ - --hash=sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc \ - --hash=sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43 \ - --hash=sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da \ - --hash=sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac \ - --hash=sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843 \ - --hash=sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e \ - --hash=sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89 \ - --hash=sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64 +rpds-py==0.19.0 \ + --hash=sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834 \ + --hash=sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4 \ + --hash=sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714 \ + --hash=sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d \ + --hash=sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22 \ + --hash=sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34 \ + --hash=sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff \ + --hash=sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb \ + --hash=sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666 \ + --hash=sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b \ + --hash=sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b \ + --hash=sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e \ + --hash=sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8 \ + --hash=sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b \ + --hash=sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582 \ + --hash=sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34 \ + --hash=sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b \ + --hash=sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581 \ + --hash=sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521 \ + --hash=sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8 \ + --hash=sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc \ + --hash=sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc \ + --hash=sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9 \ + --hash=sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c \ + --hash=sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68 \ + --hash=sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca \ + --hash=sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f \ + --hash=sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb \ + --hash=sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed \ + --hash=sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b \ + --hash=sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600 \ + --hash=sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac \ + --hash=sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5 \ + --hash=sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6 \ + --hash=sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81 \ + --hash=sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9 \ + --hash=sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08 \ + --hash=sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07 \ + --hash=sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec \ + --hash=sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526 \ + --hash=sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766 \ + --hash=sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4 \ + --hash=sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67 \ + --hash=sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c \ + --hash=sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a \ + --hash=sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479 \ + --hash=sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be \ + --hash=sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213 \ + --hash=sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa \ + --hash=sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae \ + --hash=sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf \ + --hash=sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955 \ + --hash=sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952 \ + --hash=sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f \ + --hash=sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210 \ + --hash=sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1 \ + --hash=sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd \ + --hash=sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b \ + --hash=sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c \ + --hash=sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed \ + --hash=sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55 \ + --hash=sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5 \ + --hash=sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b \ + --hash=sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e \ + --hash=sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b \ + --hash=sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a \ + --hash=sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c \ + --hash=sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378 \ + --hash=sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be \ + --hash=sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1 \ + --hash=sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0 \ + --hash=sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a \ + --hash=sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2 \ + --hash=sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633 \ + --hash=sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d \ + --hash=sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223 \ + --hash=sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa \ + --hash=sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533 \ + --hash=sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a \ + --hash=sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc \ + --hash=sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248 \ + --hash=sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05 \ + --hash=sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb \ + --hash=sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d \ + --hash=sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d \ + --hash=sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388 \ + --hash=sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9 \ + --hash=sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d \ + --hash=sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16 \ + --hash=sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2 \ + --hash=sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709 \ + --hash=sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0 \ + --hash=sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336 \ + --hash=sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c \ + --hash=sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179 \ + --hash=sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0 \ + --hash=sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb \ + --hash=sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1 \ + --hash=sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4 # via # jsonschema # referencing -ruff==0.4.10 \ - --hash=sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6 \ - --hash=sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739 \ - --hash=sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d \ - --hash=sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695 \ - --hash=sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804 \ - --hash=sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815 \ - --hash=sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac \ - --hash=sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7 \ - --hash=sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631 \ - --hash=sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e \ - --hash=sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e \ - --hash=sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef \ - --hash=sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6 \ - --hash=sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784 \ - --hash=sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81 \ - --hash=sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0 \ - --hash=sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca +ruff==0.5.1 \ + --hash=sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c \ + --hash=sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2 \ + --hash=sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655 \ + --hash=sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa \ + --hash=sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d \ + --hash=sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4 \ + --hash=sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994 \ + --hash=sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6 \ + --hash=sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d \ + --hash=sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c \ + --hash=sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78 \ + --hash=sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96 \ + --hash=sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929 \ + --hash=sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d \ + --hash=sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108 \ + --hash=sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3 \ + --hash=sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1 \ + --hash=sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0 # via -r requirements/dev.in scriv==1.5.1 \ --hash=sha256:30ae9ff8d144f8e0cf394c4e1d379542f1b3823767642955b54ec40dc00b32b6 \ --hash=sha256:a3adc657733b4124fcb54527a5f3daab0d3c300de82d0fd2b9b297b243151b78 # via -r requirements/dev.in -setuptools==70.1.1 \ - --hash=sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650 \ - --hash=sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95 +setuptools==70.3.0 \ + --hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \ + --hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc # via documenteer six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ @@ -1299,9 +1310,9 @@ termcolor==2.4.0 \ --hash=sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63 \ --hash=sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a # via pytest-sugar -tomlkit==0.12.5 \ - --hash=sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f \ - --hash=sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c +tomlkit==0.13.0 \ + --hash=sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72 \ + --hash=sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264 # via documenteer tornado==6.4.1 \ --hash=sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8 \ diff --git a/requirements/main.txt b/requirements/main.txt index 878adc00..11415a3f 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -39,13 +39,13 @@ astropy==6.1.1 \ --hash=sha256:f6660b8a588c6de0e7796c68f9a1002fad934119af8c348cb9fb5456c3e2be53 \ --hash=sha256:ffa737cac938c0a97e6839cefb844fbe382c4c972fe0629a0628ec0bd9395b51 # via pyvo -astropy-iers-data==0.2024.6.24.0.31.11 \ - --hash=sha256:0b3799034b0b76af8f915ef822d38cc90e00e235db0cb688018e4f567a8babb9 \ - --hash=sha256:ef0197b7b84dea248031e553687ea1dc58d7ac9473043693b2d33b46d81a9a12 +astropy-iers-data==0.2024.7.8.0.31.19 \ + --hash=sha256:c00af2f9b7d7bab47a03e9cfc7ef6c0c1b5bab1aaaf2a2812c4f7d4f9e8deb44 \ + --hash=sha256:dc6c14804e3d208755422eb8526a3eb3be4626e91ae707ceb705ad04170edd59 # via astropy -certifi==2024.6.2 \ - --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ - --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 +certifi==2024.7.4 \ + --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ + --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 # via # httpcore # httpx @@ -456,53 +456,58 @@ numpy==2.0.0 \ # via # astropy # pyerfa -orjson==3.10.5 \ - --hash=sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01 \ - --hash=sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa \ - --hash=sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5 \ - --hash=sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04 \ - --hash=sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d \ - --hash=sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e \ - --hash=sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b \ - --hash=sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b \ - --hash=sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268 \ - --hash=sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211 \ - --hash=sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c \ - --hash=sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c \ - --hash=sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969 \ - --hash=sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a \ - --hash=sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f \ - --hash=sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932 \ - --hash=sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26 \ - --hash=sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6 \ - --hash=sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214 \ - --hash=sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2 \ - --hash=sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f \ - --hash=sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96 \ - --hash=sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a \ - --hash=sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d \ - --hash=sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38 \ - --hash=sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807 \ - --hash=sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09 \ - --hash=sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6 \ - --hash=sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e \ - --hash=sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5 \ - --hash=sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86 \ - --hash=sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63 \ - --hash=sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2 \ - --hash=sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4 \ - --hash=sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595 \ - --hash=sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228 \ - --hash=sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9 \ - --hash=sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7 \ - --hash=sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40 \ - --hash=sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3 \ - --hash=sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139 \ - --hash=sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1 \ - --hash=sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b \ - --hash=sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47 \ - --hash=sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1 \ - --hash=sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca +orjson==3.10.6 \ + --hash=sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e \ + --hash=sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0 \ + --hash=sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f \ + --hash=sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212 \ + --hash=sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43 \ + --hash=sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b \ + --hash=sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219 \ + --hash=sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394 \ + --hash=sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a \ + --hash=sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd \ + --hash=sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844 \ + --hash=sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5 \ + --hash=sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2 \ + --hash=sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b \ + --hash=sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143 \ + --hash=sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38 \ + --hash=sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5 \ + --hash=sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148 \ + --hash=sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183 \ + --hash=sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db \ + --hash=sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a \ + --hash=sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e \ + --hash=sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6 \ + --hash=sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a \ + --hash=sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc \ + --hash=sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3 \ + --hash=sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34 \ + --hash=sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b \ + --hash=sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365 \ + --hash=sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56 \ + --hash=sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5 \ + --hash=sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863 \ + --hash=sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba \ + --hash=sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed \ + --hash=sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb \ + --hash=sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2 \ + --hash=sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0 \ + --hash=sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f \ + --hash=sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28 \ + --hash=sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a \ + --hash=sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7 \ + --hash=sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c \ + --hash=sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b \ + --hash=sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7 \ + --hash=sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb \ + --hash=sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0 \ + --hash=sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b \ + --hash=sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7 \ + --hash=sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2 \ + --hash=sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7 \ + --hash=sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b # via fastapi packaging==24.1 \ --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ @@ -512,94 +517,104 @@ pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi -pydantic==2.7.4 \ - --hash=sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52 \ - --hash=sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0 +pydantic==2.8.2 \ + --hash=sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a \ + --hash=sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8 # via # -r requirements/main.in # fastapi # pydantic-settings # safir -pydantic-core==2.18.4 \ - --hash=sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3 \ - --hash=sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8 \ - --hash=sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8 \ - --hash=sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30 \ - --hash=sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a \ - --hash=sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8 \ - --hash=sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d \ - --hash=sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc \ - --hash=sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2 \ - --hash=sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab \ - --hash=sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077 \ - --hash=sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e \ - --hash=sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9 \ - --hash=sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9 \ - --hash=sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef \ - --hash=sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1 \ - --hash=sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507 \ - --hash=sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528 \ - --hash=sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558 \ - --hash=sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b \ - --hash=sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154 \ - --hash=sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724 \ - --hash=sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695 \ - --hash=sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9 \ - --hash=sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851 \ - --hash=sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805 \ - --hash=sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a \ - --hash=sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5 \ - --hash=sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94 \ - --hash=sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c \ - --hash=sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d \ - --hash=sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef \ - --hash=sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26 \ - --hash=sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2 \ - --hash=sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c \ - --hash=sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0 \ - --hash=sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2 \ - --hash=sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4 \ - --hash=sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d \ - --hash=sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2 \ - --hash=sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce \ - --hash=sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34 \ - --hash=sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f \ - --hash=sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d \ - --hash=sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b \ - --hash=sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07 \ - --hash=sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312 \ - --hash=sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057 \ - --hash=sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d \ - --hash=sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af \ - --hash=sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb \ - --hash=sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd \ - --hash=sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78 \ - --hash=sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b \ - --hash=sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223 \ - --hash=sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a \ - --hash=sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4 \ - --hash=sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5 \ - --hash=sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23 \ - --hash=sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a \ - --hash=sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4 \ - --hash=sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8 \ - --hash=sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d \ - --hash=sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443 \ - --hash=sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e \ - --hash=sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f \ - --hash=sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e \ - --hash=sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d \ - --hash=sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc \ - --hash=sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443 \ - --hash=sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be \ - --hash=sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2 \ - --hash=sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee \ - --hash=sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f \ - --hash=sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae \ - --hash=sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864 \ - --hash=sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4 \ - --hash=sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951 \ - --hash=sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc +pydantic-core==2.20.1 \ + --hash=sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d \ + --hash=sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f \ + --hash=sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686 \ + --hash=sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482 \ + --hash=sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006 \ + --hash=sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83 \ + --hash=sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6 \ + --hash=sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88 \ + --hash=sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86 \ + --hash=sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a \ + --hash=sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6 \ + --hash=sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a \ + --hash=sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6 \ + --hash=sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6 \ + --hash=sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43 \ + --hash=sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c \ + --hash=sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4 \ + --hash=sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e \ + --hash=sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203 \ + --hash=sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd \ + --hash=sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1 \ + --hash=sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24 \ + --hash=sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc \ + --hash=sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc \ + --hash=sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3 \ + --hash=sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598 \ + --hash=sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98 \ + --hash=sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331 \ + --hash=sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2 \ + --hash=sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a \ + --hash=sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6 \ + --hash=sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688 \ + --hash=sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91 \ + --hash=sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa \ + --hash=sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b \ + --hash=sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0 \ + --hash=sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840 \ + --hash=sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c \ + --hash=sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd \ + --hash=sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3 \ + --hash=sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231 \ + --hash=sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1 \ + --hash=sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953 \ + --hash=sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250 \ + --hash=sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a \ + --hash=sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2 \ + --hash=sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20 \ + --hash=sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434 \ + --hash=sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab \ + --hash=sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703 \ + --hash=sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a \ + --hash=sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2 \ + --hash=sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac \ + --hash=sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611 \ + --hash=sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121 \ + --hash=sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e \ + --hash=sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b \ + --hash=sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09 \ + --hash=sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906 \ + --hash=sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9 \ + --hash=sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7 \ + --hash=sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b \ + --hash=sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987 \ + --hash=sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c \ + --hash=sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b \ + --hash=sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e \ + --hash=sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237 \ + --hash=sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1 \ + --hash=sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19 \ + --hash=sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b \ + --hash=sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad \ + --hash=sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0 \ + --hash=sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94 \ + --hash=sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312 \ + --hash=sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f \ + --hash=sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669 \ + --hash=sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1 \ + --hash=sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe \ + --hash=sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99 \ + --hash=sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a \ + --hash=sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a \ + --hash=sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52 \ + --hash=sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c \ + --hash=sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad \ + --hash=sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1 \ + --hash=sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a \ + --hash=sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f \ + --hash=sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a \ + --hash=sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27 # via pydantic pydantic-settings==2.3.4 \ --hash=sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a \ diff --git a/src/mobu/config.py b/src/mobu/config.py index fce30b34..7d4c1dbc 100644 --- a/src/mobu/config.py +++ b/src/mobu/config.py @@ -3,7 +3,6 @@ from __future__ import annotations from pathlib import Path -from textwrap import dedent from pydantic import Field, HttpUrl from pydantic_settings import BaseSettings @@ -11,84 +10,10 @@ __all__ = [ "Configuration", - "GitHubCiApp", - "GitHubRefreshApp", "config", ] -class GitHubCiApp(BaseSettings): - """Configuration for GitHub CI app functionality.""" - - enabled: bool = Field( - False, - title="Whether to enable the GitHub CI app functionality", - validation_alias="MOBU_GITHUB_CI_APP_ENABLED", - ) - - id: int | None = Field( - None, - title="Github CI app id", - description=( - "Found on the GitHub app's settings page (NOT the installation" - " configuration page). For example:" - " https://github.com/organizations/lsst-sqre/settings/apps/mobu-ci-data-dev-lsst-cloud" - ), - validation_alias="MOBU_GITHUB_CI_APP_ID", - examples=[123456], - ) - - private_key: str | None = Field( - None, - title="Github CI app private key", - description=( - "Generated when the GitHub app was set up. This should NOT be" - " base64 enocded, and will contain newlines. You can find this" - " in 1Password; check the Phalanx mobu values for more details." - ), - validation_alias="MOBU_GITHUB_CI_APP_PRIVATE_KEY", - examples=[ - dedent(""" - -----BEGIN RSA PRIVATE KEY----- - abc123MeowMeow456abc123MeowMeow456abc123MeowMeow456abc123MeowMeo - abc123MeowMeow456abc123MeowMeow456abc123MeowMeow456abc123MeowMeo - abc123MeowMeow456abc123MeowMeow456abc123MeowMeow456abc123MeowMeo - etc, etc - -----END RSA PRIVATE KEY----- - """) - ], - ) - - webhook_secret: str | None = Field( - None, - title="Github CI app webhook secret", - description=( - "Generated when the GitHub app was set up. You can find this" - " in 1Password; check the Phalanx mobu values for more details." - ), - validation_alias="MOBU_GITHUB_CI_APP_WEBHOOK_SECRET", - ) - - -class GitHubRefreshApp(BaseSettings): - """Configuration for GitHub refresh app functionality.""" - - enabled: bool = Field( - False, - validation_alias="MOBU_GITHUB_REFRESH_APP_ENABLED", - ) - - webhook_secret: str | None = Field( - None, - title="Github refresh app webhook secret", - description=( - "Generated when the GitHub app was set up. You can find this" - " in 1Password; check the Phalanx mobu values for more details." - ), - validation_alias="MOBU_GITHUB_REFRESH_APP_WEBHOOK_SECRET", - ) - - class Configuration(BaseSettings): """Configuration for mobu.""" @@ -115,6 +40,28 @@ class Configuration(BaseSettings): examples=["/etc/mobu/autostart.yaml"], ) + github_ci_app_config_path: Path | None = Field( + None, + title="GitHub CI app config path", + description=( + "Path to YAML file defining settings for GitHub CI app" + " integration" + ), + validation_alias="MOBU_GITHUB_CI_APP_CONFIG_PATH", + examples=["/etc/mobu/github-ci-app.yaml"], + ) + + github_refresh_app_config_path: Path | None = Field( + None, + title="GitHub refresh app config path", + description=( + "Path to YAML file defining settings for GitHub refresh app" + " integration" + ), + validation_alias="MOBU_GITHUB_REFRESH_APP_CONFIG_PATH", + examples=["/etc/mobu/github-refresh-app.yaml"], + ) + environment_url: HttpUrl | None = Field( None, title="Base URL of the Science Platform environment", @@ -140,17 +87,6 @@ class Configuration(BaseSettings): examples=["gt-vilSCi1ifK_MyuaQgMD2dQ.d6SIJhowv5Hs3GvujOyUig"], ) - github_ci_app: GitHubCiApp = Field(GitHubCiApp()) - - github_config_path: Path | None = Field( - None, - title="Path to YAML file defining settings for GitHub app integration", - validation_alias="MOBU_GITHUB_CONFIG_PATH", - examples=["/etc/mobu/github_config.yaml"], - ) - - github_refresh_app: GitHubRefreshApp = Field(GitHubRefreshApp()) - name: str = Field( "mobu", title="Name of application", diff --git a/src/mobu/constants.py b/src/mobu/constants.py index 8c3e8360..61e0f712 100644 --- a/src/mobu/constants.py +++ b/src/mobu/constants.py @@ -6,7 +6,6 @@ from pathlib import Path __all__ = [ - "GITHUB_CI_SCOPES", "GITHUB_REPO_CONFIG_PATH", "GITHUB_WEBHOOK_WAIT_SECONDS", "NOTEBOOK_REPO_BRANCH", @@ -17,14 +16,6 @@ ] -GITHUB_CI_SCOPES = [ - "exec:notebook", - "exec:portal", - "read:image", - "read:tap", -] -"""All NotebookRunner business run via GitHub CI get these scopes.""" - GITHUB_REPO_CONFIG_PATH = Path("mobu.yaml") """The path to a config file with repo-specific configuration.""" diff --git a/src/mobu/dependencies/github.py b/src/mobu/dependencies/github.py index 8de4c17a..0ef70d8d 100644 --- a/src/mobu/dependencies/github.py +++ b/src/mobu/dependencies/github.py @@ -4,25 +4,40 @@ import yaml -from ..models.github import GitHubConfig +from ..github_config import GitHubCiAppConfig, GitHubRefreshAppConfig from ..models.user import User from ..services.github_ci.ci_manager import CiManager from .context import ContextDependency -__all__ = ["GitHubConfigDependency", "CiManagerDependency"] +class GitHubCiAppConfigDependency: + """Config for GitHub CI app integration, loaded from a file.""" -class GitHubConfigDependency: - """Holds the config for GitHub app integration, loaded from a file.""" + def __init__(self) -> None: + self.config: GitHubCiAppConfig + + def __call__(self) -> GitHubCiAppConfig: + return self.config + + def initialize(self, path: Path) -> None: + self.config = GitHubCiAppConfig.model_validate( + yaml.safe_load(path.read_text()) + ) + + +class GitHubRefreshAppConfigDependency: + """Config for GitHub refresh app integration, loaded from a + file. + """ def __init__(self) -> None: - self.config: GitHubConfig + self.config: GitHubRefreshAppConfig - def __call__(self) -> GitHubConfig: + def __call__(self) -> GitHubRefreshAppConfig: return self.config def initialize(self, path: Path) -> None: - self.config = GitHubConfig.model_validate( + self.config = GitHubRefreshAppConfig.model_validate( yaml.safe_load(path.read_text()) ) @@ -36,23 +51,39 @@ class CiManagerDependency: """ def __init__(self) -> None: - self.ci_manager: CiManager + self._ci_manager: CiManager | None = None + + @property + def ci_manager(self) -> CiManager: + if self._ci_manager is None: + raise RuntimeError("CiManager has not been initialized yet") + return self._ci_manager def __call__(self) -> CiManager: return self.ci_manager def initialize( - self, base_context: ContextDependency, users: list[User] + self, + *, + base_context: ContextDependency, + users: list[User], + github_app_id: int, + github_private_key: str, + scopes: list[str], ) -> None: - self.ci_manager = CiManager( + self._ci_manager = CiManager( users=users, + github_app_id=github_app_id, + github_private_key=github_private_key, + scopes=scopes, http_client=base_context.process_context.http_client, gafaelfawr_storage=base_context.process_context.gafaelfawr, logger=base_context.process_context.logger, ) async def aclose(self) -> None: - await self.ci_manager.aclose() + if self._ci_manager is not None: + await self._ci_manager.aclose() class MaybeCiManagerDependency: @@ -68,10 +99,11 @@ def __init__(self, dep: CiManagerDependency) -> None: def __call__(self) -> CiManager | None: try: return self.dep.ci_manager - except AttributeError: + except RuntimeError: return None -github_config_dependency = GitHubConfigDependency() +github_refresh_app_config_dependency = GitHubRefreshAppConfigDependency() +github_ci_app_config_dependency = GitHubCiAppConfigDependency() ci_manager_dependency = CiManagerDependency() maybe_ci_manager_dependency = MaybeCiManagerDependency(ci_manager_dependency) diff --git a/src/mobu/github_config.py b/src/mobu/github_config.py new file mode 100644 index 00000000..8bf4a074 --- /dev/null +++ b/src/mobu/github_config.py @@ -0,0 +1,119 @@ +"""Config for GitHub application integrations.""" + +from textwrap import dedent + +from pydantic import Field +from pydantic.alias_generators import to_camel +from pydantic_settings import BaseSettings, SettingsConfigDict + +from .models.user import User + + +class GitHubCiAppConfig(BaseSettings): + """Configuration for GitHub CI app functionality if it is enabled.""" + + model_config = SettingsConfigDict( + alias_generator=to_camel, extra="forbid", populate_by_name=True + ) + + id: int = Field( + ..., + title="Github CI app id", + description=( + "Found on the GitHub app's settings page (NOT the installation" + " configuration page). For example:" + " https://github.com/organizations/lsst-sqre/settings/apps/mobu-ci-data-dev-lsst-cloud" + ), + validation_alias="MOBU_GITHUB_CI_APP_ID", + examples=[123456], + ) + + private_key: str = Field( + ..., + title="Github CI app private key", + description=( + "Generated when the GitHub app was set up. This should NOT be" + " base64 enocded, and will contain newlines. You can find this" + " in 1Password; check the Phalanx mobu values for more details." + ), + validation_alias="MOBU_GITHUB_CI_APP_PRIVATE_KEY", + examples=[ + dedent(""" + -----BEGIN RSA PRIVATE KEY----- + abc123MeowMeow456abc123MeowMeow456abc123MeowMeow456abc123MeowMeo + abc123MeowMeow456abc123MeowMeow456abc123MeowMeow456abc123MeowMeo + abc123MeowMeow456abc123MeowMeow456abc123MeowMeow456abc123MeowMeo + etc, etc + -----END RSA PRIVATE KEY----- + """) + ], + ) + + webhook_secret: str = Field( + ..., + title="Github CI app webhook secret", + description=( + "Generated when the GitHub app was set up. You can find this" + " in 1Password; check the Phalanx mobu values for more details." + ), + validation_alias="MOBU_GITHUB_CI_APP_WEBHOOK_SECRET", + ) + + users: list[User] = Field( + ..., + title="Environment users for CI jobs to run as.", + description=( + "Must be prefixed with 'bot-', like all mobu users. In " + " environments without Firestore, users have to be provisioned" + " by environment admins, and their usernames, uids, and guids must" + " be specified here. In environments with firestore, only " + " usernames need to be specified, but you still need to explicitly" + " specify as many users as needed to get the amount of concurrency" + " that you want." + ), + ) + + scopes: list[str] = Field( + ..., + title="Gafaelfawr Scopes", + description=( + "A list of Gafaelfawr scopes that will be granted to the" + " user when running notebooks for a GitHub CI app check." + ), + ) + + accepted_github_orgs: list[str] = Field( + [], + title="Allowed GitHub organizations.", + description=( + "Any webhook payload request from a repo in an organization not in" + " this list will get a 403 response." + ), + ) + + +class GitHubRefreshAppConfig(BaseSettings): + """Configuration for GitHub refresh app functionality.""" + + model_config = SettingsConfigDict( + alias_generator=to_camel, extra="forbid", populate_by_name=True + ) + + webhook_secret: str = Field( + ..., + title="Github refresh app webhook secret", + description=( + "Generated when the GitHub app was set up. You can find this" + " in 1Password; check the Phalanx mobu values for more details." + ), + validation_alias="MOBU_GITHUB_REFRESH_APP_WEBHOOK_SECRET", + ) + + accepted_github_orgs: list[str] = Field( + [], + title="Allowed GitHub organizations.", + description=( + "Any webhook payload request from a repo in an organization not in" + " this list will get a 403 response." + ), + ) diff --git a/src/mobu/handlers/github_ci_app.py b/src/mobu/handlers/github_ci_app.py index 46d8b1c4..ec180213 100644 --- a/src/mobu/handlers/github_ci_app.py +++ b/src/mobu/handlers/github_ci_app.py @@ -9,14 +9,14 @@ from safir.github.webhooks import GitHubCheckRunEventModel from safir.slack.webhook import SlackRouteErrorHandler -from ..config import config from ..constants import GITHUB_WEBHOOK_WAIT_SECONDS from ..dependencies.context import RequestContext, anonymous_context_dependency from ..dependencies.github import ( ci_manager_dependency, - github_config_dependency, + github_ci_app_config_dependency, ) -from ..models.github import GitHubCheckSuiteEventModel, GitHubConfig +from ..github_config import GitHubCiAppConfig +from ..models.github import GitHubCheckSuiteEventModel from ..services.github_ci.ci_manager import CiManager __all__ = ["api_router"] @@ -37,7 +37,9 @@ ) async def post_webhook( context: Annotated[RequestContext, Depends(anonymous_context_dependency)], - github_config: Annotated[GitHubConfig, Depends(github_config_dependency)], + ci_app_config: Annotated[ + GitHubCiAppConfig, Depends(github_ci_app_config_dependency) + ], ci_manager: Annotated[CiManager, Depends(ci_manager_dependency)], ) -> None: """Process GitHub webhook events for the mobu CI GitHubApp. @@ -45,18 +47,18 @@ async def post_webhook( Rejects webhooks from organizations that are not explicitly allowed via the mobu config. This should be exposed via a Gafaelfawr anonymous ingress. """ - webhook_secret = config.github_ci_app.webhook_secret + webhook_secret = ci_app_config.webhook_secret body = await context.request.body() event = Event.from_http( context.request.headers, body, secret=webhook_secret ) owner = event.data.get("organization", {}).get("login") - if owner not in github_config.accepted_github_orgs: + if owner not in ci_app_config.accepted_github_orgs: context.logger.debug( "Ignoring GitHub event for unaccepted org", owner=owner, - accepted_orgs=github_config.accepted_github_orgs, + accepted_orgs=ci_app_config.accepted_github_orgs, ) raise HTTPException( status_code=403, diff --git a/src/mobu/handlers/github_refresh_app.py b/src/mobu/handlers/github_refresh_app.py index a3c3971c..0aacedbc 100644 --- a/src/mobu/handlers/github_refresh_app.py +++ b/src/mobu/handlers/github_refresh_app.py @@ -9,11 +9,10 @@ from safir.github.webhooks import GitHubPushEventModel from safir.slack.webhook import SlackRouteErrorHandler -from ..config import config from ..constants import GITHUB_WEBHOOK_WAIT_SECONDS from ..dependencies.context import RequestContext, anonymous_context_dependency -from ..dependencies.github import github_config_dependency -from ..models.github import GitHubConfig +from ..dependencies.github import github_refresh_app_config_dependency +from ..github_config import GitHubRefreshAppConfig __all__ = ["api_router"] @@ -33,25 +32,28 @@ ) async def post_webhook( context: Annotated[RequestContext, Depends(anonymous_context_dependency)], - github_config: Annotated[GitHubConfig, Depends(github_config_dependency)], + refresh_app_config: Annotated[ + GitHubRefreshAppConfig, + Depends(github_refresh_app_config_dependency), + ], ) -> None: """Process GitHub webhook events for the mobu refresh GitHub app. Rejects webhooks from organizations that are not explicitly allowed via the mobu config. This should be exposed via a Gafaelfawr anonymous ingress. """ - webhook_secret = config.github_refresh_app.webhook_secret + webhook_secret = refresh_app_config.webhook_secret body = await context.request.body() event = Event.from_http( context.request.headers, body, secret=webhook_secret ) owner = event.data.get("organization", {}).get("login") - if owner not in github_config.accepted_github_orgs: + if owner not in refresh_app_config.accepted_github_orgs: context.logger.debug( "Ignoring GitHub event for unaccepted org", owner=owner, - accepted_orgs=github_config.accepted_github_orgs, + accepted_orgs=refresh_app_config.accepted_github_orgs, ) raise HTTPException( status_code=403, diff --git a/src/mobu/main.py b/src/mobu/main.py index dd84c5df..63d0006e 100644 --- a/src/mobu/main.py +++ b/src/mobu/main.py @@ -11,7 +11,7 @@ import json from collections.abc import AsyncIterator -from contextlib import AsyncExitStack, asynccontextmanager +from contextlib import asynccontextmanager from datetime import timedelta from importlib.metadata import metadata, version @@ -25,10 +25,11 @@ from .asyncio import schedule_periodic from .config import config -from .dependencies.context import ContextDependency, context_dependency +from .dependencies.context import context_dependency from .dependencies.github import ( ci_manager_dependency, - github_config_dependency, + github_ci_app_config_dependency, + github_refresh_app_config_dependency, ) from .handlers.external import external_router from .handlers.github_ci_app import api_router as github_ci_app_router @@ -42,7 +43,7 @@ @asynccontextmanager -async def base_lifespan(app: FastAPI) -> AsyncIterator[ContextDependency]: +async def lifespan(app: FastAPI) -> AsyncIterator[None]: """Set up and tear down the the base application.""" if not config.environment_url: raise RuntimeError("MOBU_ENVIRONMENT_URL was not set") @@ -55,69 +56,31 @@ async def base_lifespan(app: FastAPI) -> AsyncIterator[ContextDependency]: status_interval = timedelta(days=1) app.state.periodic_status = schedule_periodic(post_status, status_interval) - yield context_dependency - - await context_dependency.aclose() - app.state.periodic_status.cancel() - - -@asynccontextmanager -async def github_ci_app_lifespan( - base_context: ContextDependency, -) -> AsyncIterator[None]: - """Set up and tear down the GitHub CI app functionality.""" - if not config.github_config_path: - raise RuntimeError("MOBU_GITHUB_CONFIG_PATH was not set") - if not config.github_ci_app.webhook_secret: - raise RuntimeError("MOBU_GITHUB_CI_APP_WEBHOOK_SECRET was not set") - if not config.github_ci_app.private_key: - raise RuntimeError("MOBU_GITHUB_CI_APP_PRIVATE_KEY was not set") - if not config.github_ci_app.id: - raise RuntimeError("MOBU_GITHUB_CI_APP_ID was not set") - - github_config_dependency.initialize(config.github_config_path) - ci_manager_dependency.initialize( - base_context=base_context, - users=github_config_dependency.config.users, - ) - await ci_manager_dependency.ci_manager.start() - - yield - - await ci_manager_dependency.aclose() - + if config.github_refresh_app_config_path: + github_refresh_app_config_dependency.initialize( + config.github_refresh_app_config_path + ) -@asynccontextmanager -async def github_refresh_app_lifespan() -> AsyncIterator[None]: - """Set up and tear down the GitHub refresh app functionality.""" - if not config.github_config_path: - raise RuntimeError("MOBU_GITHUB_CONFIG_PATH was not set") - if not config.github_refresh_app.webhook_secret: - raise RuntimeError( - "MOBU_GITHUB_REFRESH_APP_WEBHOOK_SECRET was not set" + if config.github_ci_app_config_path: + github_ci_app_config_dependency.initialize( + config.github_ci_app_config_path + ) + ci_app_config = github_ci_app_config_dependency.config + + ci_manager_dependency.initialize( + base_context=context_dependency, + github_app_id=ci_app_config.id, + github_private_key=ci_app_config.private_key, + scopes=ci_app_config.scopes, + users=ci_app_config.users, ) - github_config_dependency.initialize(config.github_config_path) + await ci_manager_dependency.ci_manager.start() yield - -@asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncIterator[None]: - """Set up and tear down the entire application. - - Conditionally sets up and tears down the different GitHub app - integrations based on config settings. - """ - async with AsyncExitStack() as stack: - base_context = await stack.enter_async_context(base_lifespan(app)) - if config.github_ci_app.enabled: - await stack.enter_async_context( - github_ci_app_lifespan(base_context) - ) - if config.github_refresh_app.enabled: - await stack.enter_async_context(github_refresh_app_lifespan()) - - yield + await ci_manager_dependency.aclose() + await context_dependency.aclose() + app.state.periodic_status.cancel() configure_logging( @@ -141,11 +104,12 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app.include_router(internal_router) app.include_router(external_router, prefix=config.path_prefix) -if config.github_ci_app.enabled: +if config.github_ci_app_config_path: app.include_router( github_ci_app_router, prefix=f"{config.path_prefix}/github/ci" ) -if config.github_refresh_app.enabled: + +if config.github_refresh_app_config_path: app.include_router( github_refresh_app_router, prefix=f"{config.path_prefix}/github/refresh", diff --git a/src/mobu/models/ci_manager.py b/src/mobu/models/ci_manager.py index 37ac0d2a..7283e281 100644 --- a/src/mobu/models/ci_manager.py +++ b/src/mobu/models/ci_manager.py @@ -35,8 +35,8 @@ class CiWorkerSummary(BaseModel): title="User", description="User that the worker works as", examples=[ - User(username="someuser", uidnumber=None, gidnumber=None), - User(username="someuser", uidnumber=123, gidnumber=456), + User(username="bot-mobu-someuser", uidnumber=None, gidnumber=None), + User(username="bot-mobu-someuser", uidnumber=123, gidnumber=456), ], ) @@ -75,7 +75,9 @@ class CiManagerSummary(BaseModel): [ CiWorkerSummary( user=User( - username="someuser", uidnumber=123, gidnumber=456 + username="bot-mobu-someuser", + uidnumber=123, + gidnumber=456, ), num_processed=123, current_job=CiJobSummary( diff --git a/src/mobu/models/github.py b/src/mobu/models/github.py index d799adef..d9391fee 100644 --- a/src/mobu/models/github.py +++ b/src/mobu/models/github.py @@ -5,53 +5,14 @@ import safir.github.models import safir.github.webhooks -from pydantic import BaseModel, Field, field_validator - -from .user import User +from pydantic import Field __all__ = [ "GitHubCheckSuiteEventModel", "GitHubCheckSuiteModel", - "GitHubConfig", ] -class GitHubConfig(BaseModel): - """Config for the GitHub CI app funcionality.""" - - users: list[User] = Field( - [], - title="Environment users for CI jobs to run as.", - description=( - "Must be prefixed with 'bot-', like all mobu users. In " - " environments without Firestore, users have to be provisioned" - " by environment admins, and their usernames, uids, and guids must" - " be specified here. In environments with firestore, only " - " usernames need to be specified, but you still need to explicitly" - " specify as many users as needed to get the amount of concurrency" - " that you want." - ), - ) - accepted_github_orgs: list[str] = Field( - [], - title="GitHub organizations to accept webhook requests from.", - description=( - "Any webhook payload request from a repo in an organization not in" - " this list will get a 403 response." - ), - ) - - @field_validator("users") - @classmethod - def check_bot_user(cls, v: list[User]) -> list[User]: - bad = [u.username for u in v if not u.username.startswith("bot-")] - if any(bad): - raise ValueError( - f"All usernames must start with 'bot-'. These don't: {bad}" - ) - return v - - class GitHubCheckSuiteModel( safir.github.models.GitHubCheckSuiteModel, ): diff --git a/src/mobu/models/user.py b/src/mobu/models/user.py index d5cede55..26760583 100644 --- a/src/mobu/models/user.py +++ b/src/mobu/models/user.py @@ -14,7 +14,13 @@ class User(BaseModel): """Configuration for the user whose credentials the monkey will use.""" - username: str = Field(..., title="Username", examples=["testuser"]) + username: str = Field( + ..., + title="Username", + description="Must start with 'bot-mobu'", + pattern=r"^bot-mobu", + examples=["bot-mobu-testuser"], + ) uidnumber: int | None = Field( None, @@ -46,7 +52,7 @@ class UserSpec(BaseModel): ..., title="Prefix for usernames", description="Each user will be formed by appending a number to this", - examples=["lsptestuser"], + examples=["bot-mobu-lsptestuser"], ) uid_start: int | None = Field( diff --git a/src/mobu/services/github_ci/ci_manager.py b/src/mobu/services/github_ci/ci_manager.py index 8e9b1f3b..e30b7342 100644 --- a/src/mobu/services/github_ci/ci_manager.py +++ b/src/mobu/services/github_ci/ci_manager.py @@ -70,11 +70,15 @@ class CiManager: def __init__( self, + github_app_id: int, + github_private_key: str, + scopes: list[str], users: list[User], http_client: AsyncClient, gafaelfawr_storage: GafaelfawrStorage, logger: BoundLogger, ) -> None: + self._scopes = scopes self._users = users self._gafaelfawr = gafaelfawr_storage self._http_client = http_client @@ -87,15 +91,9 @@ def __init__( # Used for deterministic testing self.lifecycle = CiManagerLifecycle() - if not config.github_ci_app.id: - raise RuntimeError("MOBU_GITHUB_CI_APP_ID was not set") - if not config.github_ci_app.webhook_secret: - raise RuntimeError("MOBU_GITHUB_CI_APP_WEBHOOK_SECRET was not set") - if not config.github_ci_app.private_key: - raise RuntimeError("MOBU_GITHUB_CI_APP_PRIVATE_KEY was not set") self._factory = GitHubAppClientFactory( - id=config.github_ci_app.id, - key=config.github_ci_app.private_key, + id=github_app_id, + key=github_private_key, name="lsst-sqre/mobu CI app", http_client=http_client, ) @@ -106,6 +104,7 @@ async def start(self) -> None: self.workers = [ Worker( user=user, + scopes=self._scopes, queue=self._queue, logger=self._logger, ) @@ -272,6 +271,8 @@ class Worker: Parameters ---------- + scopes + A list of Gafaelfawr scopes granted to the job's user user The user to do the work as. queue @@ -282,10 +283,13 @@ class Worker: def __init__( self, + *, + scopes: list[str], user: User, queue: Queue[QueueItem], logger: BoundLogger, ) -> None: + self._scopes = scopes self._user = user self._queue = queue self._logger = logger.bind(ci_worker=user.username) @@ -312,7 +316,7 @@ async def run(self) -> None: f"Processing job: {job}, with user: {self._user}" ) - await job.run(user=self._user) + await job.run(user=self._user, scopes=self._scopes) lifecycle.processed.set() self.current_job = None diff --git a/src/mobu/services/github_ci/ci_notebook_job.py b/src/mobu/services/github_ci/ci_notebook_job.py index 52157975..d38ebba9 100644 --- a/src/mobu/services/github_ci/ci_notebook_job.py +++ b/src/mobu/services/github_ci/ci_notebook_job.py @@ -7,7 +7,7 @@ from httpx import AsyncClient from structlog.stdlib import BoundLogger -from mobu.constants import GITHUB_CI_SCOPES, GITHUB_REPO_CONFIG_PATH +from mobu.constants import GITHUB_REPO_CONFIG_PATH from mobu.exceptions import GitHubFileNotFoundError from mobu.models.business.notebookrunner import ( NotebookRunnerConfig, @@ -56,7 +56,7 @@ def __init__( self._logger = logger.bind(ci_job_type="NotebookJob") self._notebooks: list[Path] = [] - async def run(self, user: User) -> None: + async def run(self, user: User, scopes: list[str]) -> None: """Run all relevant changed notebooks and report back to GitHub. Run only changed notebooks that aren't excluded in the mobu config @@ -107,7 +107,7 @@ async def run(self, user: User) -> None: await self.check_run.start(summary=summary) solitary_config = SolitaryConfig( user=user, - scopes=GITHUB_CI_SCOPES, + scopes=[str(scope) for scope in scopes], business=NotebookRunnerConfig( type="NotebookRunner", options=NotebookRunnerOptions( diff --git a/tests/autostart_test.py b/tests/autostart_test.py index 8e99f9ef..0ef8b961 100644 --- a/tests/autostart_test.py +++ b/tests/autostart_test.py @@ -20,7 +20,7 @@ - name: basic count: 10 user_spec: - username_prefix: testuser + username_prefix: bot-mobu-testuser uid_start: 1000 gid_start: 2000 scopes: ["exec:notebook"] @@ -29,9 +29,9 @@ - name: python count: 2 users: - - username: python + - username: bot-mobu-python uidnumber: 60000 - - username: otherpython + - username: bot-mobu-otherpython uidnumber: 70000 scopes: ["exec:notebook"] restart: true @@ -64,7 +64,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: assert r.status_code == 200 expected_monkeys = [ { - "name": f"testuser{i:02d}", + "name": f"bot-mobu-testuser{i:02d}", "business": { "failure_count": 0, "name": "EmptyLoop", @@ -78,7 +78,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: "token": ANY, "uidnumber": 1000 + i - 1, "gidnumber": 2000 + i - 1, - "username": f"testuser{i:02d}", + "username": f"bot-mobu-testuser{i:02d}", }, } for i in range(1, 11) @@ -89,7 +89,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: "name": "basic", "count": 10, "user_spec": { - "username_prefix": "testuser", + "username_prefix": "bot-mobu-testuser", "uid_start": 1000, "gid_start": 2000, }, @@ -109,11 +109,11 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: "count": 2, "users": [ { - "username": "python", + "username": "bot-mobu-python", "uidnumber": 60000, }, { - "username": "otherpython", + "username": "bot-mobu-otherpython", "uidnumber": 70000, }, ], @@ -132,7 +132,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: }, "monkeys": [ { - "name": "python", + "name": "bot-mobu-python", "business": { "failure_count": 0, "image": { @@ -150,13 +150,13 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "python", + "username": "bot-mobu-python", "uidnumber": 60000, "gidnumber": 60000, }, }, { - "name": "otherpython", + "name": "bot-mobu-otherpython", "business": { "failure_count": 0, "image": { @@ -174,7 +174,7 @@ async def test_autostart(client: AsyncClient, jupyter: MockJupyter) -> None: "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "otherpython", + "username": "bot-mobu-otherpython", "uidnumber": 70000, "gidnumber": 70000, }, diff --git a/tests/business/gitlfs_test.py b/tests/business/gitlfs_test.py index 9a1c4552..736beec9 100644 --- a/tests/business/gitlfs_test.py +++ b/tests/business/gitlfs_test.py @@ -25,9 +25,9 @@ async def test_run( # Wait until we've finished at least one loop, # then check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "GitLFSBusiness", @@ -39,12 +39,12 @@ async def test_run( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } # Get the log and check that we logged the query. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 assert "Running Git-LFS check..." in r.text assert "Git-LFS check finished after " in r.text @@ -65,9 +65,9 @@ async def test_fail(client: AsyncClient, respx_mock: respx.Router) -> None: # We expect it to have failed in the git push, because we don't really # have a Git LFS server for it to talk to. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 1, "name": "GitLFSBusiness", @@ -79,12 +79,12 @@ async def test_fail(client: AsyncClient, respx_mock: respx.Router) -> None: "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } # Get the log and check that we logged the query. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 assert "Running Git-LFS check..." in r.text assert ("mobu.exceptions.SubprocessError") in r.text diff --git a/tests/business/notebookrunner_test.py b/tests/business/notebookrunner_test.py index f332c10c..6fcbd778 100644 --- a/tests/business/notebookrunner_test.py +++ b/tests/business/notebookrunner_test.py @@ -56,7 +56,7 @@ async def test_run( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -74,9 +74,9 @@ async def test_run( assert r.status_code == 201 # Wait until we've finished one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "NotebookRunner", @@ -89,14 +89,14 @@ async def test_run( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } finally: os.chdir(cwd) # Get the log and check the cell output. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 # Root notebook @@ -138,7 +138,7 @@ async def test_run_recursive( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -156,9 +156,9 @@ async def test_run_recursive( assert r.status_code == 201 # Wait until we've finished one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "NotebookRunner", @@ -171,14 +171,14 @@ async def test_run_recursive( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } finally: os.chdir(cwd) # Get the log and check the cell output. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 # Root notebook @@ -236,7 +236,7 @@ async def test_refresh( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -256,7 +256,7 @@ async def test_refresh( # We should see a message from the notebook execution in the logs. assert await wait_for_log_message( - client, "testuser1", msg="This is a test" + client, "bot-mobu-testuser1", msg="This is a test" ) # Change the notebook and git commit it @@ -278,12 +278,12 @@ async def test_refresh( # The refresh should have forced a new execution assert await wait_for_log_message( - client, "testuser1", msg="Deleting lab" + client, "bot-mobu-testuser1", msg="Deleting lab" ) # We should see a message from the updated notebook. assert await wait_for_log_message( - client, "testuser1", msg="This is a NEW test" + client, "bot-mobu-testuser1", msg="This is a NEW test" ) finally: os.chdir(cwd) @@ -316,7 +316,7 @@ async def test_exclude_dirs( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -338,9 +338,9 @@ async def test_exclude_dirs( assert r.status_code == 201 # Wait until we've finished one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "NotebookRunner", @@ -353,14 +353,14 @@ async def test_exclude_dirs( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } finally: os.chdir(cwd) # Get the log and check the cell output. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 # Root notebook @@ -413,7 +413,7 @@ async def test_alert( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -431,9 +431,9 @@ async def test_alert( assert r.status_code == 201 # Wait until we've finished one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 1, "image": { @@ -453,7 +453,7 @@ async def test_alert( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } @@ -481,12 +481,12 @@ async def test_alert( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { diff --git a/tests/business/nubladopythonloop_test.py b/tests/business/nubladopythonloop_test.py index d87c2e95..5b4f83b7 100644 --- a/tests/business/nubladopythonloop_test.py +++ b/tests/business/nubladopythonloop_test.py @@ -23,14 +23,19 @@ async def test_run( client: AsyncClient, jupyter: MockJupyter, respx_mock: respx.Router ) -> None: - mock_gafaelfawr(respx_mock, username="testuser1", uid=1000, gid=1000) + mock_gafaelfawr( + respx_mock, username="bot-mobu-testuser1", uid=1000, gid=1000 + ) r = await client.put( "/mobu/flocks", json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser", "uid_start": 1000}, + "user_spec": { + "username_prefix": "bot-mobu-testuser", + "uid_start": 1000, + }, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -41,9 +46,9 @@ async def test_run( assert r.status_code == 201 # Wait until we've finished one loop. Make sure nothing fails. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "NubladoPythonLoop", @@ -57,14 +62,14 @@ async def test_run( "token": ANY, "uidnumber": 1000, "gidnumber": 1000, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } # Check that the lab is shut down properly between iterations. - assert jupyter.state["testuser1"] == JupyterState.LOGGED_IN + assert jupyter.state["bot-mobu-testuser1"] == JupyterState.LOGGED_IN - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 assert "Starting up" in r.text assert ": Server requested" in r.text @@ -87,7 +92,7 @@ async def test_reuse_lab( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -103,11 +108,11 @@ async def test_reuse_lab( assert r.status_code == 201 # Wait until we've finished one loop. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["failure_count"] == 0 # Check that the lab is still running between iterations. - assert jupyter.state["testuser1"] == JupyterState.LAB_RUNNING + assert jupyter.state["bot-mobu-testuser1"] == JupyterState.LAB_RUNNING @pytest.mark.asyncio @@ -121,7 +126,7 @@ async def test_server_shutdown( json={ "name": "test", "count": 20, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -149,7 +154,7 @@ async def test_delayed_delete( json={ "name": "test", "count": 5, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -175,14 +180,14 @@ async def test_hub_failed( respx_mock: respx.Router, ) -> None: mock_gafaelfawr(respx_mock) - jupyter.fail("testuser2", JupyterAction.SPAWN) + jupyter.fail("bot-mobu-testuser2", JupyterAction.SPAWN) r = await client.put( "/mobu/flocks", json={ "name": "test", "count": 2, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -193,7 +198,7 @@ async def test_hub_failed( assert r.status_code == 201 # Wait until we've finished one loop. - data = await wait_for_business(client, "testuser2") + data = await wait_for_business(client, "bot-mobu-testuser2") assert data["business"]["success_count"] == 0 assert data["business"]["failure_count"] > 0 @@ -223,12 +228,12 @@ async def test_hub_failed( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser2", + "text": "*Monkey*\ntest/bot-mobu-testuser2", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser2", + "text": "*User*\nbot-mobu-testuser2", "verbatim": True, }, { @@ -267,7 +272,7 @@ async def test_redirect_loop( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -278,7 +283,7 @@ async def test_redirect_loop( assert r.status_code == 201 # Wait until we've finished one loop. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["success_count"] == 0 assert data["business"]["failure_count"] > 0 @@ -286,7 +291,7 @@ async def test_redirect_loop( assert config.environment_url url = urljoin( str(config.environment_url), - "/nb/hub/api/users/testuser1/server/progress", + "/nb/hub/api/users/bot-mobu-testuser1/server/progress", ) assert slack.messages == [ { @@ -314,12 +319,12 @@ async def test_redirect_loop( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -358,7 +363,7 @@ async def test_spawn_timeout( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -370,7 +375,7 @@ async def test_spawn_timeout( # Wait for one loop to finish. We should finish with an error fairly # quickly (one second) and post a timeout alert to Slack. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["success_count"] == 0 assert data["business"]["failure_count"] > 0 assert slack.messages == [ @@ -396,12 +401,12 @@ async def test_spawn_timeout( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -425,14 +430,14 @@ async def test_spawn_failed( respx_mock: respx.Router, ) -> None: mock_gafaelfawr(respx_mock) - jupyter.fail("testuser1", JupyterAction.PROGRESS) + jupyter.fail("bot-mobu-testuser1", JupyterAction.PROGRESS) r = await client.put( "/mobu/flocks", json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -444,7 +449,7 @@ async def test_spawn_failed( # Wait for one loop to finish. We should finish with an error fairly # quickly (one second) and post a timeout alert to Slack. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["success_count"] == 0 assert data["business"]["failure_count"] > 0 assert slack.messages == [ @@ -470,12 +475,12 @@ async def test_spawn_failed( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -520,7 +525,7 @@ async def test_delete_timeout( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -537,7 +542,7 @@ async def test_delete_timeout( # Wait for one loop to finish. We should finish with an error fairly # quickly (one second) and post a delete timeout alert to Slack. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["success_count"] == 0 assert data["business"]["failure_count"] > 0 assert slack.messages == [ @@ -563,12 +568,12 @@ async def test_delete_timeout( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -600,7 +605,7 @@ async def test_code_exception( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -615,7 +620,7 @@ async def test_code_exception( assert r.status_code == 201 # Wait until we've finished one loop. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["failure_count"] == 1 # Check that an appropriate error was posted. @@ -642,12 +647,12 @@ async def test_code_exception( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -716,7 +721,7 @@ async def test_long_error( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -738,11 +743,11 @@ async def test_long_error( assert r.status_code == 201 # Wait until we've finished one loop. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["failure_count"] == 1 # Check the lab form. - assert jupyter.lab_form["testuser1"] == { + assert jupyter.lab_form["bot-mobu-testuser1"] == { "image_list": ( "registry.hub.docker.com/lsstsqre/sciplat-lab:d_2021_08_30" ), @@ -778,12 +783,12 @@ async def test_long_error( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -848,7 +853,7 @@ async def test_lab_controller( json={ "name": "test", "count": 1, - "users": [{"username": "testuser"}], + "users": [{"username": "bot-mobu-testuser"}], "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -866,7 +871,7 @@ async def test_lab_controller( ) assert r.status_code == 201 await asyncio.sleep(0) - assert jupyter.lab_form["testuser"] == { + assert jupyter.lab_form["bot-mobu-testuser"] == { "image_list": ( "registry.hub.docker.com/lsstsqre/sciplat-lab:d_2021_08_30" ), @@ -881,7 +886,7 @@ async def test_lab_controller( json={ "name": "test", "count": 1, - "users": [{"username": "testuser"}], + "users": [{"username": "bot-mobu-testuser"}], "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -897,7 +902,7 @@ async def test_lab_controller( ) assert r.status_code == 201 await asyncio.sleep(0) - assert jupyter.lab_form["testuser"] == { + assert jupyter.lab_form["bot-mobu-testuser"] == { "enable_debug": "true", "image_class": "latest-daily", "size": "Medium", @@ -911,7 +916,7 @@ async def test_lab_controller( json={ "name": "test", "count": 1, - "users": [{"username": "testuser"}], + "users": [{"username": "bot-mobu-testuser"}], "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -927,7 +932,7 @@ async def test_lab_controller( ) assert r.status_code == 201 await asyncio.sleep(0) - assert jupyter.lab_form["testuser"] == { + assert jupyter.lab_form["bot-mobu-testuser"] == { "image_tag": "w_2077_44", "size": "Small", } @@ -947,7 +952,7 @@ async def test_ansi_error( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -971,7 +976,7 @@ async def test_ansi_error( assert r.status_code == 201 # Wait until we've finished one loop. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["failure_count"] == 1 # Check that an appropriate error was posted. @@ -998,12 +1003,12 @@ async def test_ansi_error( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { diff --git a/tests/business/tapqueryrunner_test.py b/tests/business/tapqueryrunner_test.py index c5facc4e..412f01c6 100644 --- a/tests/business/tapqueryrunner_test.py +++ b/tests/business/tapqueryrunner_test.py @@ -27,7 +27,7 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": { "type": "TAPQueryRunner", @@ -38,9 +38,9 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.status_code == 201 # Wait until we've finished at least one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "TAPQueryRunner", @@ -52,12 +52,14 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } # Get the log and check that we logged the query. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get( + "/mobu/flocks/test/monkeys/bot-mobu-testuser1/log" + ) assert r.status_code == 200 assert "Running (sync): " in r.text found = False diff --git a/tests/business/tapquerysetrunner_test.py b/tests/business/tapquerysetrunner_test.py index d6de9da1..966abe70 100644 --- a/tests/business/tapquerysetrunner_test.py +++ b/tests/business/tapquerysetrunner_test.py @@ -34,7 +34,7 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": {"type": "TAPQuerySetRunner"}, }, @@ -42,9 +42,9 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: assert r.status_code == 201 # Wait until we've finished at least one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data == { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "TAPQuerySetRunner", @@ -56,12 +56,14 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, } # Get the log and check that we logged the query. - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get( + "/mobu/flocks/test/monkeys/bot-mobu-testuser1/log" + ) assert r.status_code == 200 assert "Running (sync): " in r.text assert "Query finished after " in r.text @@ -85,7 +87,7 @@ async def test_setup_error( json={ "name": "test", "count": 1, - "users": [{"username": "tapuser"}], + "users": [{"username": "bot-mobu-tapuser"}], "scopes": ["exec:notebook"], "business": {"type": "TAPQuerySetRunner"}, }, @@ -93,7 +95,7 @@ async def test_setup_error( assert r.status_code == 201 # Wait until we've finished at least one loop and check the results. - data = await wait_for_business(client, "tapuser") + data = await wait_for_business(client, "bot-mobu-tapuser") assert data["business"]["failure_count"] == 1 assert slack.messages == [ @@ -122,12 +124,12 @@ async def test_setup_error( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/tapuser", + "text": "*Monkey*\ntest/bot-mobu-tapuser", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntapuser", + "text": "*User*\nbot-mobu-tapuser", "verbatim": True, }, { @@ -157,7 +159,7 @@ async def test_alert( json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": {"type": "TAPQuerySetRunner"}, }, @@ -165,7 +167,7 @@ async def test_alert( assert r.status_code == 201 # Wait until we've finished at least one loop and check the results. - data = await wait_for_business(client, "testuser1") + data = await wait_for_business(client, "bot-mobu-testuser1") assert data["business"]["failure_count"] == 1 assert slack.messages == [ @@ -191,12 +193,12 @@ async def test_alert( }, { "type": "mrkdwn", - "text": "*Monkey*\ntest/testuser1", + "text": "*Monkey*\ntest/bot-mobu-testuser1", "verbatim": True, }, { "type": "mrkdwn", - "text": "*User*\ntestuser1", + "text": "*User*\nbot-mobu-testuser1", "verbatim": True, }, { @@ -249,7 +251,7 @@ async def test_random_object() -> None: objects = [str(o) for o in yaml.safe_load(f)["object_ids"]] user = AuthenticatedUser( - username="user", scopes=["read:tap"], token="blah blah" + username="bot-mobu-user", scopes=["read:tap"], token="blah blah" ) logger = structlog.get_logger(__file__) options = TAPQuerySetRunnerOptions(query_set=query_set) diff --git a/tests/conftest.py b/tests/conftest.py index 3961e512..fd61ab82 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ from safir.testing.slack import MockSlackWebhook, mock_slack_webhook from mobu import main -from mobu.config import GitHubCiApp, GitHubRefreshApp, config +from mobu.config import config from mobu.services.business.gitlfs import GitLFSBusiness from .support.constants import ( @@ -62,67 +62,78 @@ def _configure() -> Iterator[None]: @pytest.fixture -def _enable_github_ci_app(tmp_path: Path) -> Iterator[None]: +def _enable_github_ci_app( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Iterator[None]: """Enable the GitHub CI app functionality. We need to reload the main module here because including the router is done conditionally on module import. """ - github_config = tmp_path / "github_config.yaml" + github_config = tmp_path / "github_ci_app_config.yaml" github_config.write_text( dedent(""" - users: - - username: bot-mobu-unittest-1 - - username: bot-mobu-unittest-2 - accepted_github_orgs: - - org1 - - org2 - - lsst-sqre + users: + - username: bot-mobu-unittest-1 + - username: bot-mobu-unittest-2 + accepted_github_orgs: + - org1 + - org2 + - lsst-sqre + scopes: + - "exec:notebook" + - "exec:portal" + - "read:image" + - "read:tap" """) ) - config.github_ci_app.id = 1 - config.github_ci_app.enabled = True - config.github_ci_app.webhook_secret = TEST_GITHUB_CI_APP_SECRET - config.github_ci_app.private_key = TEST_GITHUB_CI_APP_PRIVATE_KEY - config.github_config_path = github_config + monkeypatch.setenv("MOBU_GITHUB_CI_APP_ID", "1") + monkeypatch.setenv( + "MOBU_GITHUB_CI_APP_WEBHOOK_SECRET", TEST_GITHUB_CI_APP_SECRET + ) + monkeypatch.setenv( + "MOBU_GITHUB_CI_APP_PRIVATE_KEY", TEST_GITHUB_CI_APP_PRIVATE_KEY + ) + monkeypatch.setattr(config, "github_ci_app_config_path", github_config) + reload(main) yield - config.github_ci_app = GitHubCiApp() - config.github_config_path = None reload(main) @pytest.fixture -def _enable_github_refresh_app(tmp_path: Path) -> Iterator[None]: - """Enable the GitHub Refresh app routes. +def _enable_github_refresh_app( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Iterator[None]: + """Enable the GitHub refresh app functionality. We need to reload the main module here because including the router is done conditionally on module import. """ - github_config = tmp_path / "github_config.yaml" + github_config = tmp_path / "github_ci_app_refresh.yaml" github_config.write_text( dedent(""" - users: - - username: bot-mobu-unittest-1 - - username: bot-mobu-unittest-2 - accepted_github_orgs: - - org1 - - org2 - - lsst-sqre + accepted_github_orgs: + - org1 + - org2 + - lsst-sqre """) ) + monkeypatch.setenv("MOBU_GITHUB_REFRESH_APP_ID", "1") + monkeypatch.setenv( + "MOBU_GITHUB_REFRESH_APP_WEBHOOK_SECRET", + TEST_GITHUB_REFRESH_APP_SECRET, + ) + monkeypatch.setattr( + config, "github_refresh_app_config_path", github_config + ) - config.github_refresh_app.enabled = True - config.github_refresh_app.webhook_secret = TEST_GITHUB_REFRESH_APP_SECRET - config.github_config_path = github_config reload(main) yield - config.github_refresh_app = GitHubRefreshApp() - config.github_config_path = None reload(main) diff --git a/tests/handlers/flock_test.py b/tests/handlers/flock_test.py index c868a256..09b3eb40 100644 --- a/tests/handlers/flock_test.py +++ b/tests/handlers/flock_test.py @@ -30,7 +30,7 @@ async def test_start_stop_refresh( config = { "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, } @@ -41,13 +41,13 @@ async def test_start_stop_refresh( "config": { "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, }, "monkeys": [ { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "EmptyLoop", @@ -59,14 +59,14 @@ async def test_start_stop_refresh( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, }, ], } assert r.json() == expected assert r.headers["Location"] == f"{TEST_BASE_URL}/mobu/flocks/test" - await wait_for_business(client, "testuser1") + await wait_for_business(client, "bot-mobu-testuser1") r = await client.get("/mobu/flocks") assert r.status_code == 200 @@ -83,17 +83,17 @@ async def test_start_stop_refresh( r = await client.get("/mobu/flocks/test/monkeys") assert r.status_code == 200 - assert r.json() == ["testuser1"] + assert r.json() == ["bot-mobu-testuser1"] - r = await client.get("/mobu/flocks/test/monkeys/testuser1") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1") assert r.status_code == 200 assert r.json() == expected["monkeys"][0] - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 200 assert "text/plain" in r.headers["Content-Type"] assert "filename" in r.headers["Content-Disposition"] - assert "test-testuser1-" in r.headers["Content-Disposition"] + assert "test-bot-mobu-testuser1-" in r.headers["Content-Disposition"] assert "Idling..." in r.text r = await client.get("/mobu/flocks/test/summary") @@ -120,9 +120,9 @@ async def test_start_stop_refresh( assert r.status_code == 404 r = await client.get("/mobu/flocks/other/monkeys") assert r.status_code == 404 - r = await client.get("/mobu/flocks/test/monkeys/testuser2") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser2") assert r.status_code == 404 - r = await client.get("/mobu/flocks/test/monkeys/testuser2/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser2/log") assert r.status_code == 404 r = await client.delete("/mobu/flocks/test") @@ -132,9 +132,9 @@ async def test_start_stop_refresh( assert r.status_code == 404 r = await client.get("/mobu/flocks/test/monkeys") assert r.status_code == 404 - r = await client.get("/mobu/flocks/test/monkeys/testuser1") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1") assert r.status_code == 404 - r = await client.get("/mobu/flocks/test/monkeys/testuser1/log") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser1/log") assert r.status_code == 404 r = await client.get("/mobu/flocks") @@ -152,8 +152,12 @@ async def test_user_list( "name": "test", "count": 2, "users": [ - {"username": "testuser", "uidnumber": 1000, "gidnumber": 1056}, - {"username": "otheruser", "uidnumber": 60000}, + { + "username": "bot-mobu-testuser", + "uidnumber": 1000, + "gidnumber": 1056, + }, + {"username": "bot-mobu-otheruser", "uidnumber": 60000}, ], "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, @@ -165,7 +169,7 @@ async def test_user_list( "config": config, "monkeys": [ { - "name": "testuser", + "name": "bot-mobu-testuser", "business": { "failure_count": 0, "name": "EmptyLoop", @@ -179,11 +183,11 @@ async def test_user_list( "token": ANY, "uidnumber": 1000, "gidnumber": 1056, - "username": "testuser", + "username": "bot-mobu-testuser", }, }, { - "name": "otheruser", + "name": "bot-mobu-otheruser", "business": { "failure_count": 0, "name": "EmptyLoop", @@ -197,7 +201,7 @@ async def test_user_list( "token": ANY, "uidnumber": 60000, "gidnumber": 60000, - "username": "otheruser", + "username": "bot-mobu-otheruser", }, }, ], @@ -205,11 +209,11 @@ async def test_user_list( assert r.json() == expected assert r.headers["Location"] == f"{TEST_BASE_URL}/mobu/flocks/test" - r = await client.get("/mobu/flocks/test/monkeys/testuser") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-testuser") assert r.status_code == 200 assert r.json() == expected["monkeys"][0] - r = await client.get("/mobu/flocks/test/monkeys/otheruser") + r = await client.get("/mobu/flocks/test/monkeys/bot-mobu-otheruser") assert r.status_code == 200 assert r.json() == expected["monkeys"][1] @@ -228,10 +232,13 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: "name": "test", "count": 2, "users": [ - {"username": "testuser", "uidnumber": 1000}, - {"username": "otheruser", "uidnumber": 60000}, + {"username": "bot-mobu-testuser", "uidnumber": 1000}, + {"username": "bot-mobu-otheruser", "uidnumber": 60000}, ], - "user_spec": {"username_prefix": "testuser", "uid_start": 1000}, + "user_spec": { + "username_prefix": "bot-mobu-testuser", + "uid_start": 1000, + }, "scopes": [], "business": {"type": "EmptyLoop"}, }, @@ -281,9 +288,9 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: "name": "test", "count": 2, "users": [ - {"username": "testuser", "uidnumber": 1000}, - {"username": "otheruser", "uidnumber": 60000}, - {"username": "thirduser", "uidnumber": 70000}, + {"username": "bot-mobu-testuser", "uidnumber": 1000}, + {"username": "bot-mobu-otheruser", "uidnumber": 60000}, + {"username": "bot-mobu-thirduser", "uidnumber": 70000}, ], "scopes": [], "business": {"type": "EmptyLoop"}, @@ -308,7 +315,7 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: json={ "name": "test", "count": 2, - "users": [{"username": "testuser", "uidnumber": 1000}], + "users": [{"username": "bot-mobu-testuser", "uidnumber": 1000}], "scopes": [], "business": {"type": "EmptyLoop"}, }, @@ -332,7 +339,10 @@ async def test_errors(client: AsyncClient, respx_mock: respx.Router) -> None: json={ "name": "test", "count": 1, - "user_spec": {"username_prefix": "testuser", "uid_start": 1000}, + "user_spec": { + "username_prefix": "bot-mobu-testuser", + "uid_start": 1000, + }, "scopes": ["exec:notebook"], "business": {"type": "UnknownBusiness"}, }, diff --git a/tests/handlers/github_refresh_app_test.py b/tests/handlers/github_refresh_app_test.py index 7dfee93b..d2619291 100644 --- a/tests/handlers/github_refresh_app_test.py +++ b/tests/handlers/github_refresh_app_test.py @@ -105,7 +105,7 @@ async def test_handle_webhook( { "name": "test-notebook", "count": 1, - "user_spec": {"username_prefix": "testuser-notebook"}, + "user_spec": {"username_prefix": "bot-mobu-testuser-notebook"}, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -118,7 +118,9 @@ async def test_handle_webhook( { "name": "test-notebook-branch", "count": 1, - "user_spec": {"username_prefix": "testuser-notebook-branch"}, + "user_spec": { + "username_prefix": "bot-mobu-testuser-notebook-branch" + }, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -131,7 +133,9 @@ async def test_handle_webhook( { "name": "test-other-notebook", "count": 1, - "user_spec": {"username_prefix": "testuser-other-notebook"}, + "user_spec": { + "username_prefix": "bot-mobu-testuser-other-notebook" + }, "scopes": ["exec:notebook"], "business": { "type": "NotebookRunner", @@ -144,7 +148,7 @@ async def test_handle_webhook( { "name": "test-non-notebook", "count": 1, - "user_spec": {"username_prefix": "testuser-non-notebook"}, + "user_spec": {"username_prefix": "bot-mobu-testuser-non-notebook"}, "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, }, @@ -173,22 +177,22 @@ async def test_handle_webhook( # Only the business for the correct branch and repo should be refreshing r = await client.get( - "/mobu/flocks/test-notebook/monkeys/testuser-notebook1" + "/mobu/flocks/test-notebook/monkeys/bot-mobu-testuser-notebook1" ) assert r.json()["business"]["refreshing"] is True # The other businesses should not be refreshing r = await client.get( - "/mobu/flocks/test-notebook-branch/monkeys/testuser-notebook-branch1" + "/mobu/flocks/test-notebook-branch/monkeys/bot-mobu-testuser-notebook-branch1" ) assert r.json()["business"]["refreshing"] is False r = await client.get( - "/mobu/flocks/test-other-notebook/monkeys/testuser-other-notebook1" + "/mobu/flocks/test-other-notebook/monkeys/bot-mobu-testuser-other-notebook1" ) assert r.json()["business"]["refreshing"] is False r = await client.get( - "/mobu/flocks/test-non-notebook/monkeys/testuser-non-notebook1" + "/mobu/flocks/test-non-notebook/monkeys/bot-mobu-testuser-non-notebook1" ) assert r.json()["business"]["refreshing"] is False diff --git a/tests/handlers/solitary_test.py b/tests/handlers/solitary_test.py index 7a28e166..faa424aa 100644 --- a/tests/handlers/solitary_test.py +++ b/tests/handlers/solitary_test.py @@ -19,7 +19,7 @@ async def test_run(client: AsyncClient, respx_mock: respx.Router) -> None: r = await client.post( "/mobu/run", json={ - "user": {"username": "solitary"}, + "user": {"username": "bot-mobu-solitary"}, "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, }, @@ -40,7 +40,7 @@ async def test_error( r = await client.post( "/mobu/run", json={ - "user": {"username": "solitary"}, + "user": {"username": "bot-mobu-solitary"}, "scopes": ["exec:notebook"], "business": { "type": "NubladoPythonLoop", @@ -54,6 +54,8 @@ async def test_error( assert r.status_code == 200 result = r.json() assert result == {"success": False, "error": ANY, "log": ANY} - assert "solitary: running code 'raise Exception" in result["error"] + assert ( + "bot-mobu-solitary: running code 'raise Exception" in result["error"] + ) assert "Exception: some error\n" in result["error"] assert "Exception: some error" in result["log"] diff --git a/tests/monkeyflocker_test.py b/tests/monkeyflocker_test.py index b17deb68..4c9ad8ca 100644 --- a/tests/monkeyflocker_test.py +++ b/tests/monkeyflocker_test.py @@ -23,7 +23,7 @@ name: basic count: 1 user_spec: - username_prefix: testuser + username_prefix: bot-mobu-testuser scopes: ["exec:notebook"] business: type: EmptyLoop @@ -77,13 +77,13 @@ def test_start_report_refresh_stop( "config": { "name": "basic", "count": 1, - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], "business": {"type": "EmptyLoop"}, }, "monkeys": [ { - "name": "testuser1", + "name": "bot-mobu-testuser1", "business": { "failure_count": 0, "name": "EmptyLoop", @@ -95,7 +95,7 @@ def test_start_report_refresh_stop( "user": { "scopes": ["exec:notebook"], "token": ANY, - "username": "testuser1", + "username": "bot-mobu-testuser1", }, }, ], @@ -120,9 +120,9 @@ def test_start_report_refresh_stop( ) assert result.exit_code == 0 - with (output_path / "testuser1_stats.json").open("r") as f: + with (output_path / "bot-mobu-testuser1_stats.json").open("r") as f: assert expected["monkeys"][0] == json.load(f) - log = (output_path / "testuser1_log.txt").read_text() + log = (output_path / "bot-mobu-testuser1_log.txt").read_text() assert "Idling..." in log shutil.rmtree(str(output_path)) @@ -159,9 +159,9 @@ def test_start_report_refresh_stop( ) assert result.exit_code == 0 - with (output_path / "testuser1_stats.json").open("r") as f: + with (output_path / "bot-mobu-testuser1_stats.json").open("r") as f: assert expected["monkeys"][0] == json.load(f) - log = (output_path / "testuser1_log.txt").read_text() + log = (output_path / "bot-mobu-testuser1_log.txt").read_text() assert "Idling..." in log r = httpx.get(f"{monkeyflocker_app.url}/mobu/flocks/basic") diff --git a/tests/services/ci_manager_test.py b/tests/services/ci_manager_test.py index 11f90358..01ca146c 100644 --- a/tests/services/ci_manager_test.py +++ b/tests/services/ci_manager_test.py @@ -18,6 +18,7 @@ from mobu.services.business.base import Business from mobu.services.github_ci.ci_manager import CiManager from mobu.storage.gafaelfawr import GafaelfawrStorage +from tests.support.constants import TEST_GITHUB_CI_APP_PRIVATE_KEY from ..support.gafaelfawr import mock_gafaelfawr from ..support.github import GitHubMocker, MockJob @@ -25,14 +26,16 @@ def create_ci_manager(respx_mock: respx.Router) -> CiManager: """Create a CiManger with appropriately mocked dependencies.""" + scopes = [ + "exec_notebook", + "exec_portal", + "read_image", + "read_tap", + ] + mock_gafaelfawr( respx_mock, - scopes=[ - "exec:notebook", - "exec:portal", - "read:image", - "read:tap", - ], + scopes=[str(scope) for scope in scopes], ) http_client = AsyncClient() logger = structlog.get_logger() @@ -42,7 +45,13 @@ def create_ci_manager(respx_mock: respx.Router) -> CiManager: http_client=http_client, gafaelfawr_storage=gafaelfawr, logger=logger, - users=[User(username="user1"), User(username="user2")], + scopes=scopes, + github_app_id=123, + github_private_key=TEST_GITHUB_CI_APP_PRIVATE_KEY, + users=[ + User(username="bot-mobu-user1"), + User(username="bot-mobu-user2"), + ], ) @@ -86,12 +95,16 @@ async def test_stops_on_empty_queue( expected_summary = CiManagerSummary( workers=[ CiWorkerSummary( - user=User(username="user1", uidnumber=None, gidnumber=None), + user=User( + username="bot-mobu-user1", uidnumber=None, gidnumber=None + ), num_processed=0, current_job=None, ), CiWorkerSummary( - user=User(username="user2", uidnumber=None, gidnumber=None), + user=User( + username="bot-mobu-user2", uidnumber=None, gidnumber=None + ), num_processed=0, current_job=None, ), @@ -310,7 +323,9 @@ async def test_shutdown( expected_summary1 = CiManagerSummary( workers=[ CiWorkerSummary( - user=User(username="user1", uidnumber=None, gidnumber=None), + user=User( + username="bot-mobu-user1", uidnumber=None, gidnumber=None + ), num_processed=1, current_job=CiJobSummary( commit_url=HttpUrl( @@ -320,7 +335,9 @@ async def test_shutdown( ), ), CiWorkerSummary( - user=User(username="user2", uidnumber=None, gidnumber=None), + user=User( + username="bot-mobu-user2", uidnumber=None, gidnumber=None + ), num_processed=1, current_job=CiJobSummary( commit_url=HttpUrl( @@ -336,7 +353,9 @@ async def test_shutdown( expected_summary2 = CiManagerSummary( workers=[ CiWorkerSummary( - user=User(username="user1", uidnumber=None, gidnumber=None), + user=User( + username="bot-mobu-user1", uidnumber=None, gidnumber=None + ), num_processed=1, current_job=CiJobSummary( commit_url=HttpUrl( @@ -346,7 +365,9 @@ async def test_shutdown( ), ), CiWorkerSummary( - user=User(username="user2", uidnumber=None, gidnumber=None), + user=User( + username="bot-mobu-user2", uidnumber=None, gidnumber=None + ), num_processed=1, current_job=CiJobSummary( commit_url=HttpUrl( diff --git a/tests/storage/gafaelfawr_test.py b/tests/storage/gafaelfawr_test.py index 5095a116..6f3cb3ef 100644 --- a/tests/storage/gafaelfawr_test.py +++ b/tests/storage/gafaelfawr_test.py @@ -15,8 +15,8 @@ @pytest.mark.asyncio async def test_generate_token(respx_mock: respx.Router) -> None: - mock_gafaelfawr(respx_mock, "someuser", 1234, 1234) - config = User(username="someuser", uidnumber=1234) + mock_gafaelfawr(respx_mock, "bot-mobu-someuser", 1234, 1234) + config = User(username="bot-mobu-someuser", uidnumber=1234) scopes = ["exec:notebook"] client = await http_client_dependency() @@ -24,7 +24,7 @@ async def test_generate_token(respx_mock: respx.Router) -> None: gafaelfawr = GafaelfawrStorage(client, logger) user = await gafaelfawr.create_service_token(config, scopes) - assert user.username == "someuser" + assert user.username == "bot-mobu-someuser" assert user.uidnumber == 1234 assert user.gidnumber == 1234 assert user.scopes == ["exec:notebook"] diff --git a/tests/support/gitlfs.py b/tests/support/gitlfs.py index 5e2fd023..ceabb409 100644 --- a/tests/support/gitlfs.py +++ b/tests/support/gitlfs.py @@ -11,7 +11,7 @@ "name": "test", "count": 1, "debug": "true", - "user_spec": {"username_prefix": "testuser"}, + "user_spec": {"username_prefix": "bot-mobu-testuser"}, "scopes": ["exec:notebook"], # IRL it would need write:git-lfs "business": { "type": "GitLFS",