diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9eae8d8..1c3986c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,15 @@ name: CI +env: + # Current supported Python version. For applications, there is generally no + # reason to support multiple Python versions, so all actions are run with + # this version. Quote the version to avoid interpretation as a floating + # point number. + PYTHON_VERSION: "3.12" + "on": + merge_group: {} + pull_request: {} push: branches-ignore: # These should always correspond to pull requests, so ignore them for @@ -8,53 +17,48 @@ name: CI # trigger, avoiding running the workflow twice. This is a minor # optimization so there's no need to ensure this is comprehensive. - "dependabot/**" + - "gh-readonly-queue/**" - "renovate/**" - "tickets/**" - "u/**" tags: - "*" - pull_request: {} jobs: - test: + lint: runs-on: ubuntu-latest - - strategy: - matrix: - python: - - "3.9" + timeout-minutes: 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python }} + python-version: ${{ env.PYTHON_VERSION }} - name: Run pre-commit - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 - - name: Install tox - run: pip install tox + test: + runs-on: ubuntu-latest + timeout-minutes: 10 - - name: Cache tox environments - id: cache-tox - uses: actions/cache@v3 - with: - path: .tox - # requirements/*.txt and pyproject.toml have versioning info - # that would impact the tox environment. - key: tox-${{ matrix.python }}-${{ hashFiles('requirements/*.txt') }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - tox-${{ matrix.python }}-${{ hashFiles('requirements/*.txt') }}- + steps: + - uses: actions/checkout@v4 - - name: Run tox - run: tox -e py,coverage-report + - name: Run nox + uses: lsst-sqre/run-nox@v1 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache-dependency: "requirements/*.txt" + nox-package: "nox[uv] testcontainers[kafka]" + nox-sessions: "typing test" build: runs-on: ubuntu-latest - needs: [test] + needs: [lint, test] + timeout-minutes: 10 # Only do Docker builds of tagged releases and pull requests from ticket # branches. This will still trigger on pull requests from untrusted @@ -62,45 +66,17 @@ jobs: # but in this case the build will fail with an error since the secret # won't be set. if: > - startsWith(github.ref, 'refs/tags/') - || startsWith(github.head_ref, 'tickets/') + github.event_name != 'merge_group' + && (startsWith(github.ref, 'refs/tags/') + || startsWith(github.head_ref, 'tickets/')) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Define the Docker tag - id: vars - run: echo ::set-output name=tag::$(scripts/docker-tag.sh) - - - name: Print the tag - id: print - run: echo ${{ steps.vars.outputs.tag }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v3 + - uses: lsst-sqre/build-and-push-to-ghcr@v1 + id: build with: - context: . - push: true - tags: | - lsstsqre/templatebot:${{ steps.vars.outputs.tag }} - ghcr.io/lsst-sqre/templatebot:${{ steps.vars.outputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max + image: ${{ github.repository }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/periodic-ci.yaml b/.github/workflows/periodic-ci.yaml new file mode 100644 index 0000000..09f0791 --- /dev/null +++ b/.github/workflows/periodic-ci.yaml @@ -0,0 +1,57 @@ +# This is a separate run of the Python test suite that runs from a schedule, +# doesn't cache the tox environment, and updates pinned dependencies first. +# The purpose is to test compatibility with the latest versions of +# dependencies. + +name: Periodic CI + +env: + # Current supported Python version. For applications, there is generally no + # reason to support multiple Python versions, so all actions are run with + # this version. Quote the version to avoid interpretation as a floating + # point number. + PYTHON_VERSION: "3.12" + +"on": + schedule: + - cron: "0 12 * * 1" + workflow_dispatch: {} + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Update dependencies + run: | + pip install --upgrade uv + uv venv + source .venv/bin/activate + make update-deps + shell: bash + + - name: Run nox + uses: lsst-sqre/run-nox@v1 + with: + python-version: ${{ env.PYTHON_VERSION }} + nox-sessions: "lint typing test" + nox-package: "nox[uv] testcontainers[kafka]" + use-cache: false + + - name: Report status + if: failure() + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ job.status }} + notify_when: "failure" + notification_title: "Periodic test for {repo} failed" + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ALERT_WEBHOOK }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1687d7a..239015a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,14 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - - id: check-yaml - id: check-toml + - id: check-yaml + - id: trailing-whitespace - - repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - additional_dependencies: - - toml - - - repo: https://github.com/psf/black - rev: 22.6.0 - hooks: - - id: black - - - repo: https://gitlab.com/pycqa/flake8 - rev: 4.0.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 hooks: - - id: flake8 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9647482 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,130 @@ +# Change log + + + + +## 0.3.0 (2024-10-08) + +### New features + +- This is an all-new version of templatebot built for the modern Squarebot. It uses Faststream and FastAPI with Pydantic models for modelling the event payloads. Templatebot now also combines the previous templatebot with lsst-templatebot-aide to provide a single backend to handle project bootstrapping for Rubin Observatory projects. + +## 0.2.0 (2021-12-01) + +- Change PR for lsst-texmf to use main branches, not master. +- Update to GitHub Actions from Travis CI +- Modernize packaging to meet SQuaRE's current standards (tox, pre-commit, formatting with Black and isort, pip-tools compiled dependencies and multi-stage docker image build). + +## 0.1.2 (2020-06-30) + +- Improve messaging to the Slack user (always "at" the user). + +## 0.1.1 (2020-06-15) + +- Update aiokafka to 0.6.0. + This should resolve the uncaught UnknownMemberId exception that was causing the templatebot Kafka consumer to drop its connection to the Kafka brokers. + +- Update kafkit to 0.2.0b3. + +- Updated testing stack to pytest 5.4.3 and pytest-flake8 to 1.0.6. + +- Updated GitPython to 3.1.3 to resolve a floating dependency error related to the `gitdb.utils.compat` module. + +- Updated templatekit to 0.4.1, matching the version used by [lsst/templates](https://github.com/lsst/templates). This change also allowed us to float the version of Click so that it would be set by cookiecutter/templatekit. + +## 0.1.0 (2019-11-29) + +This release focuses on improving the deployment with Kustomize, better configurability, and support for connecting to Kafka brokers through TLS. + +- Templatebot can now be deployed through Kustomize. The base is located at `/manifests/base`. This means that you can incorporate this application into a specific Kustomize-based application (such as one deployed by Argo CD) with a URL such as `github.com/lsst-sqre/templatebot.git//manifests/base?ref=0.1.0`. There is a separate template for the Secret resource expected by the deployment at `/manifest/base/secret.template.yaml`. + +- Topics names can now be configured directly. See the environment variables: + + - `TEMPLATEBOT_TOPIC_PRERENDER` + - `TEMPLATEBOT_TOPIC_RENDERREADY` + - `TEMPLATEBOT_TOPIC_POSTRENDER` + - `SQRBOTJR_TOPIC_APP_MENTION` + - `SQRBOTJR_TOPIC_MESSAGE_IM` + - `SQRBOTJR_TOPIC_INTERACTION` + + This granular configuration allows you to consume production topics, but output development topics, for example. + +- The old "staging version" configuration is now the `TEMPLATEBOT_SUBJECT_SUFFIX` environment variable. This configuration is used solely as a suffix on the fully-qualified name of a schema when determining its subject name at the Schema Registry. Previously it also impacted topic names. Use a subject suffix when trying out new Avro schemas to avoid polluting the production subject in the registry. + +- Templatebot can now connect to Kafka brokers through SSL. Set the `KAFKA_PROTOCOL` environment variable to `SSL`. Then set these environment variables to the paths of specific TLS certificates and keys: + + - `KAFKA_CLUSTER_CA` (the Kafka cluster's CA certificate) + - `KAFKA_CLIENT_CA` (Templatebot's client CA certificate) + - `KAFKA_CLIENT_CERT` (Templatebot's client certificate) + - `KAFKA_CLIENT_KEY` (Templatebot's client key) + +- The consumer group IDs of the sqrbot-topic and templatebot-aide topic consumers can now be set independently with these environment variables: + + - `TEMPLATEBOT_SLACK_GROUP_ID` + - `TEMPLATEBOT_EVENTS_GROUP_ID` + + It's a good idea to set these consumers to have different groups to avoid apparent race conditions when starting up. + +- Individual features can be enabled or disabled: + + - `TEMPLATEBOT_ENABLE_SLACK_CONSUMER`: set to `"0"` to disable consuming events from sqrbot. + - `TEMPLATEBOT_ENABLE_EVENTS_CONSUMER`: set to `"0"` to disable consuming events from templatebot-aide. + - `TEMPLATEBOT_TOPIC_CONFIG`: set to `"0"` to disable configuring topics if they do not already exist. + +## 0.0.8 (2019-11-04) + +- Templatebot now responds to the user typing "help." + +## 0.0.7 (2019-11-04) + +- Templatebot now routinely checks if the template repository clone is up-to-date with the origin remote. + These checks are done whenever the template repository is being accessed, for instance in the handlers that list templates, that present template dialogs in Slack, or in rendering a template. + A template repository is only re-cloned if the local SHA does not match the SHA of the symbolic Git ref (branch or tag) on the origin. + +## 0.0.6 (2019-10-14) + +- Update templatekit to 0.3.0. + +## 0.0.5 (2019-05-02) + +- Templatebot now uses the `master` branch of https://github.com/lsst/templates by default. + +- Templatebot now responds to `app_mention` Slack events. This means that you can be in a public channel and type `@sqrbot-jr create file`. Templatebot continues to monitor direct messages. + +## 0.0.4 (2019-04-16) + +This release builds out the ability for Templatebot to trigger pre- and post-rendering events to domain-specific helper applications. For LSST, this helper microservice is [lsst-templatebot-aide](https://github.com/lsst-sqre/lsst-templatebot-aide). The sequence of events is: + +1. Templatebot receives the `sqrbot-interaction` event from Slack dialog closure for files or projects. For project templates, Templatebot emits a `templatebot-prerender` event that gets picked up by the `lsst-templatebot-aide` or equivalent external microservice. + +2. The helper microservice provisions the repository on GitHub. This allows a helper to do specialized work to select and provision a GitHub repository. For example, to determine the serial number for a template's repository. The helper emits a `templatebot-render_ready` event. + +3. Templatebot creates the first commit for the new repository based on the Cookiecutter template and then emits a `templatebot-postrender` event. + +4. The helper application receives the `templatebot-postrender` event and does additional configuration, such as activating CI and documentation services. + +This release also includes Kubernetes deployment manifests. + +## 0.0.3 (2019-03-18) + +This release focuses on refining the user experience of creating a file or project from Slack using the `templatekit.yaml` configuration files introduced in Templatekit 0.2.0. + +- In the initial template selection menus, template names and groupings are derived from `templatekit.yaml` configurations. Templates are now better organized and better labeled! + +- Fields in the dialog are driven by the `dialog_fields` field in `templatekit.yaml` configurations (Templatekit will still provide a default set of fields if none are set). These configurations, defined in Templatekit 0.2.0+ allow for exciting UI features like labels, placeholders, and hints. The schema validator in Templatekit ensures that labels aren't too long, and that there aren't too many dialog fields — this makes the dialog implementation in Templatebot much simpler. + + These configurations also introduce the concept of _preset menus_, which combine multiple cookiecutter variable presets into selection menu options. This feature lets us handle complicated templates, which many boolean or constrained option variables, within the five-field limit imposed by Slack dialogs. + +- This release also includes a handler for project templates, though only as a proof-of-concept for showing that cookiecutter variables for complex templates like `stack_package` can be successfully captured. + +## 0.0.2 (2019-03-12) + +This release focuses on file template creation (`@sqrbot-jr create file`): + +- A new `RepoManager` class manages clones of the template repository (a Git repo). The `RepoManager` caches clones by Git SHA and clones are immutable. What this means is that one handler can be rendering a template from the `master` branch while a new handler sees that `master` is updated and begins a new clone of `master`. + +- The file template handler now populates the Slack dialog with actual fields from the template's `cookiecutter.json` file and renders the actual template with templatekit. The filename is also rendered from the cookiecutter context. + +## 0.0.1 (2019-02-21) + +This is the initial proof-of-concept of Templatebot. It implements a SQuaRE Events (Kafka) listener and mocks up an interaction with a Slack-based user creating a file template. Templatebot opens a Slack dialog to get specific information needed by a template, and then uploads the generated file back to the channel. [See this PR for a demo gif](https://github.com/lsst-sqre/templatebot/pull/1#issuecomment-466219231). diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index b30c7a1..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,164 +0,0 @@ -########## -Change log -########## - -0.2.0 (2021-12-01) -================== - -- Change PR for lsst-texmf to use main branches, not master. -- Update to GitHub Actions from Travis CI -- Modernize packaging to meet SQuaRE's current standards (tox, pre-commit, formatting with Black and isort, pip-tools compiled dependencies and multi-stage docker image build). - -0.1.2 (2020-06-30) -================== - -- Improve messaging to the Slack user (always "at" the user). - -0.1.1 (2020-06-15) -================== - -- Update aiokafka to 0.6.0. - This should resolve the uncaught UnknownMemberId exception that was causing the templatebot Kafka consumer to drop its connection to the Kafka brokers. - -- Update kafkit to 0.2.0b3. - -- Updated testing stack to pytest 5.4.3 and pytest-flake8 to 1.0.6. - -- Updated GitPython to 3.1.3 to resolve a floating dependency error related to the ``gitdb.utils.compat`` module. - -- Updated templatekit to 0.4.1, matching the version used by `lsst/templates `__. - This change also allowed us to float the version of Click so that it would be set by cookiecutter/templatekit. - -0.1.0 (2019-11-29) -================== - -This release focuses on improving the deployment with Kustomize, better configurability, and support for connecting to Kafka brokers through TLS. - -- Templatebot can now be deployed through Kustomize. - The base is located at ``/manifests/base``. - This means that you can incorporate this application into a specific Kustomize-based application (such as one deployed by Argo CD) with a URL such as ``github.com/lsst-sqre/templatebot.git//manifests/base?ref=0.1.0``. - There is a separate template for the Secret resource expected by the deployment at ``/manifest/base/secret.template.yaml``. - -- Topics names can now be configured directly. - See the environment variables: - - - ``TEMPLATEBOT_TOPIC_PRERENDER`` - - ``TEMPLATEBOT_TOPIC_RENDERREADY`` - - ``TEMPLATEBOT_TOPIC_POSTRENDER`` - - ``SQRBOTJR_TOPIC_APP_MENTION`` - - ``SQRBOTJR_TOPIC_MESSAGE_IM`` - - ``SQRBOTJR_TOPIC_INTERACTION`` - - This granular configuration allows you to consume production topics, but output development topics, for example. - -- The old "staging version" configuration is now the ``TEMPLATEBOT_SUBJECT_SUFFIX`` environment variable. - This configuration is used solely as a suffix on the fully-qualified name of a schema when determining its subject name at the Schema Registry. - Previously it also impacted topic names. - Use a subject suffix when trying out new Avro schemas to avoid polluting the production subject in the registry. - -- Templatebot can now connect to Kafka brokers through SSL. - Set the ``KAFKA_PROTOCOL`` environment variable to ``SSL``. - Then set these environment variables to the paths of specific TLS certificates and keys: - - - ``KAFKA_CLUSTER_CA`` (the Kafka cluster's CA certificate) - - ``KAFKA_CLIENT_CA`` (Templatebot's client CA certificate) - - ``KAFKA_CLIENT_CERT`` (Templatebot's client certificate) - - ``KAFKA_CLIENT_KEY`` (Templatebot's client key) - -- The consumer group IDs of the sqrbot-topic and templatebot-aide topic consumers can now be set independently with these environment variables: - - - ``TEMPLATEBOT_SLACK_GROUP_ID`` - - ``TEMPLATEBOT_EVENTS_GROUP_ID`` - - It's a good idea to set these consumers to have different groups to avoid apparent race conditions when starting up. - -- Individual features can be enabled or disabled: - - - ``TEMPLATEBOT_ENABLE_SLACK_CONSUMER``: set to ``"0"`` to disable consuming events from sqrbot. - - ``TEMPLATEBOT_ENABLE_EVENTS_CONSUMER``: set to ``"0"`` to disable consuming events from templatebot-aide. - - ``TEMPLATEBOT_TOPIC_CONFIG``: set to ``"0"`` to disable configuring topics if they do not already exist. - -0.0.8 (2019-11-04) -================== - -- Templatebot now responds to the user typing "help." - -0.0.7 (2019-11-04) -================== - -- Templatebot now routinely checks if the template repository clone is up-to-date with the origin remote. - These checks are done whenever the template repository is being accessed, for instance in the handlers that list templates, that present template dialogs in Slack, or in rendering a template. - A template repository is only re-cloned if the local SHA does not match the SHA of the symbolic Git ref (branch or tag) on the origin. - -0.0.6 (2019-10-14) -================== - -- Update templatekit to 0.3.0. - -0.0.5 (2019-05-02) -================== - -- Templatebot now uses the ``master`` branch of https://github.com/lsst/templates by default. - -- Templatebot now responds to ``app_mention`` Slack events. - This means that you can be in a public channel and type ``@sqrbot-jr create file``. - Templatebot continues to monitor direct messages. - -0.0.4 (2019-04-16) -================== - -This release builds out the ability for Templatebot to trigger pre- and post-rendering events to domain-specific helper applications. -For LSST, this helper microservice is `lsst-templatebot-aide `__. -The sequence of events is: - -1. Templatebot receives the ``sqrbot-interaction`` event from Slack dialog closure for files or projects. - For project templates, Templatebot emits a ``templatebot-prerender`` event that gets picked up by the ``lsst-templatebot-aide`` or equivalent external microservice. - -2. The helper microservice provisions the repository on GitHub. - This allows a helper to do specialized work to select and provision a GitHub repository. - For example, to determine the serial number for a template's repository. - The helper emits a ``templatebot-render_ready`` event. - -3. Templatebot creates the first commit for the new repository based on the Cookiecutter template and then emits a ``templatebot-postrender`` event. - -4. The helper application receives the ``templatebot-postrender`` event and does additional configuration, such as activating CI and documentation services. - -This release also includes Kubernetes deployment manifests. - -0.0.3 (2019-03-18) -================== - -This release focuses on refining the user experience of creating a file or project from Slack using the ``templatekit.yaml`` configuration files introduced in Templatekit 0.2.0. - -- In the initial template selection menus, template names and groupings are derived from ``templatekit.yaml`` configurations. - Templates are now better organized and better labeled! - -- Fields in the dialog are driven by the ``dialog_fields`` field in ``templatekit.yaml`` configurations (Templatekit will still provide a default set of fields if none are set). - These configurations, defined in Templatekit 0.2.0+ allow for exciting UI features like labels, placeholders, and hints. - The schema validator in Templatekit ensures that labels aren't too long, and that there aren't too many dialog fields — this makes the dialog implementation in Templatebot much simpler. - - These configurations also introduce the concept of *preset menus*, which combine multiple cookiecutter variable presets into selection menu options. - This feature lets us handle complicated templates, which many boolean or constrained option variables, within the five-field limit imposed by Slack dialogs. - -- This release also includes a handler for project templates, though only as a proof-of-concept for showing that cookiecutter variables for complex templates like ``stack_package`` can be successfully captured. - -0.0.2 (2019-03-12) -================== - -This release focuses on file template creation (``@sqrbot-jr create file``): - -- A new ``RepoManager`` class manages clones of the template repository (a Git repo). - The ``RepoManager`` caches clones by Git SHA and clones are immutable. - What this means is that one handler can be rendering a template from the ``master`` branch while a new handler sees that ``master`` is updated and begins a new clone of ``master``. - -- The file template handler now populates the Slack dialog with actual fields from the template's ``cookiecutter.json`` file and renders the actual template with templatekit. - The filename is also rendered from the cookiecutter context. - -0.0.1 (2019-02-21) -================== - -This is the initial proof-of-concept of Templatebot. -It implements a SQuaRE Events (Kafka) listener and mocks up an interaction with a Slack-based user creating a file template. -Templatebot opens a Slack dialog to get specific information needed by a template, and then uploads the generated file back to the channel. `See this PR for a demo gif `__. - -:jirab:`DM-17865` diff --git a/Dockerfile b/Dockerfile index cae8cf6..7830e0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,61 +1,58 @@ -# This Dockerfile has four stages: +# This Dockerfile has three stages: # # base-image # Updates the base Python image with security patches and common system # packages. This image becomes the base of all other images. -# dependencies-image -# Installs third-party dependencies (requirements/main.txt) into a virtual -# environment. This virtual environment is ideal for copying across build -# stages. # install-image -# Installs the app into the virtual environment. +# Installs third-party dependencies (requirements/main.txt) and the +# application into a virtual environment. This virtual environment is ideal +# for copying across build stages. # runtime-image # - Copies the virtual environment into place. # - Runs a non-root user. # - Sets up the entrypoint and port. -FROM python:3.10.7-slim-bullseye as base-image +FROM python:3.12.6-slim-bookworm AS base-image -# Update system packages +# Update system packages. COPY scripts/install-base-packages.sh . RUN ./install-base-packages.sh && rm ./install-base-packages.sh -FROM base-image AS dependencies-image +FROM base-image AS install-image + +# Install uv. +COPY --from=ghcr.io/astral-sh/uv:0.4.9 /uv /bin/uv # Install system packages only needed for building dependencies. COPY scripts/install-dependency-packages.sh . RUN ./install-dependency-packages.sh -# Create a Python virtual environment +# Create a Python virtual environment. ENV VIRTUAL_ENV=/opt/venv -RUN python -m venv $VIRTUAL_ENV -# Make sure we use the virtualenv +RUN uv venv $VIRTUAL_ENV + +# Make sure we use the virtualenv. ENV PATH="$VIRTUAL_ENV/bin:$PATH" -# Put the latest pip and setuptools in the virtualenv -RUN pip install --upgrade --no-cache-dir pip setuptools wheel -# Install the app's Python runtime dependencies +# Install the app's Python runtime dependencies. COPY requirements/main.txt ./requirements.txt -RUN pip install --quiet --no-cache-dir -r requirements.txt - -FROM dependencies-image AS install-image - -# Use the virtualenv -ENV PATH="/opt/venv/bin:$PATH" +RUN uv pip install --compile-bytecode --verify-hashes --no-cache \ + -r requirements.txt +# Install the application itself. COPY . /workdir WORKDIR /workdir -RUN pip install --no-cache-dir . +RUN uv pip install --compile-bytecode --no-cache . FROM base-image AS runtime-image -# Create a non-root user +# Create a non-root user. RUN useradd --create-home appuser -# Copy the virtualenv +# Copy the virtualenv. COPY --from=install-image /opt/venv /opt/venv -# Make sure we use the virtualenv +# Make sure we use the virtualenv. ENV PATH="/opt/venv/bin:$PATH" # Switch to the non-root user. @@ -65,4 +62,4 @@ USER appuser EXPOSE 8080 # Run the application. -CMD ["templatebot", "run", "--port", "8080"] +CMD ["uvicorn", "templatebot.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/Makefile b/Makefile index 809ff4b..ceedf3b 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,36 @@ -.PHONY: update-deps -update-deps: - pip install --upgrade pip-tools pip setuptools - pip-compile --upgrade --build-isolation --generate-hashes --output-file requirements/main.txt requirements/main.in - pip-compile --upgrade --build-isolation --generate-hashes --output-file requirements/dev.txt requirements/dev.in - -.PHONY: update-deps-no-hashes -update-deps-no-hashes: - pip install --upgrade pip-tools pip setuptools - pip-compile --upgrade --build-isolation --allow-unsafe --output-file requirements/main.txt requirements/main.in - pip-compile --upgrade --build-isolation --allow-unsafe --output-file requirements/dev.txt requirements/dev.in +.PHONY: help +help: + @echo "Make targets for example" + @echo "make init - Set up dev environment" + @echo "make run - Start a local development instance" + @echo "make update - Update pinned dependencies and run make init" + @echo "make update-deps - Update pinned dependencies" + @echo "make update-deps-no-hashes - Pin dependencies without hashes" .PHONY: init init: - pip install --editable . - pip install --upgrade -r requirements/main.txt -r requirements/dev.txt + pip install --upgrade uv + uv pip install -r requirements/main.txt -r requirements/dev.txt + uv pip install --editable . rm -rf .tox - pip install --upgrade tox pre-commit + uv pip install --upgrade pre-commit pre-commit install +.PHONY: run +run: + tox run -e run + .PHONY: update update: update-deps init -.PHONY: run -run: - tox -e run +.PHONY: update-deps +update-deps: + pip install --upgrade uv + uv pip install --upgrade pre-commit + pre-commit autoupdate + uv pip compile --upgrade --universal --generate-hashes \ + --output-file requirements/main.txt requirements/main.in + uv pip compile --upgrade --universal --generate-hashes \ + --output-file requirements/dev.txt requirements/dev.in + uv pip compile --upgrade --universal --generate-hashes \ + --output-file requirements/tox.txt requirements/tox.in diff --git a/README.md b/README.md new file mode 100644 index 0000000..d443874 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Templatebot + +Templatebot creates new projects and files based on templates in Rubin Observatory's https://github.com/lsst/templates repository. +Templatebot works with the [Squarebot](https://github.com/lsst-sqre/squarebot) Slack front-end. + +## Development + +To bootstrap a development environment, create a virtual environment and install nox: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -U nox +python -m nox -s init +``` + +To run the tests: + +```bash +python -m nox +``` + +Individual sessions are: + +- `init`: Install pre-commit hooks +- `lint`: Run linters through pre-commit +- `typing`: Run mypy +- `test`: Run tests (requires Docker to run testcontainers) diff --git a/README.rst b/README.rst deleted file mode 100644 index bbca1cb..0000000 --- a/README.rst +++ /dev/null @@ -1,7 +0,0 @@ -########### -Templatebot -########### - -Templatebot creates new projects and files based on templates in the Large Synoptic Survey Telescope's https://github.com/lsst/templates repository. -It's still under construction, but it's intended to operated from both an HTTP API and Slack (with https://github.com/lsst/sqrbot-jr). -Templatebot is deployed as a microservice on the api.lsst.codes platform. diff --git a/changelog.d/_template.md.jinja b/changelog.d/_template.md.jinja new file mode 100644 index 0000000..6e644b8 --- /dev/null +++ b/changelog.d/_template.md.jinja @@ -0,0 +1,7 @@ + +{%- for cat in config.categories %} + +### {{ cat }} + +- +{%- endfor %} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 120000 index 0000000..04c99a5 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 120000 index e22698b..0000000 --- a/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -../CHANGELOG.rst \ No newline at end of file diff --git a/manifests/base/configmap.yaml b/manifests/base/configmap.yaml deleted file mode 100644 index c97d589..0000000 --- a/manifests/base/configmap.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: templatebot -data: - # These configurations are injected as environment variables into the - # templatebot app container. - API_LSST_CODES_NAME: 'templatebot' - API_LSST_CODES_PROFILE: "production" - REGISTRY_URL: "http://localhost:8081" - KAFKA_BROKER: "localhost:9092" - KAFKA_PROTOCOL: "PLAINTEXT" - # KAFKA_CLUSTER_CA: "" - # KAFKA_CLIENT_CA: "" - # KAFKA_CLIENT_CERT: "" - # KAFKA_CLIENT_KEY: "" - TEMPLATEBOT_SUBJECT_SUFFIX: '' - TEMPLATEBOT_SUBJECT_COMPATIBILITY: 'FORWARD_TRANSITIVE' - TEMPLATEBOT_SLACK_GROUP_ID: 'templatebot' - TEMPLATEBOT_EVENTS_GROUP_ID: 'templatebot' - TEMPLATEBOT_TOPIC_PRERENDER: 'templatebot.prerender' - TEMPLATEBOT_TOPIC_RENDERREADY: 'templatebot.render-ready' - TEMPLATEBOT_TOPIC_POSTRENDER: 'templatebot.postrender' - SQRBOTJR_TOPIC_APP_MENTION: 'sqrbot.app.mention' - SQRBOTJR_TOPIC_MESSAGE_IM: 'sqrbot.message.im' - SQRBOTJR_TOPIC_INTERACTION: 'sqrbot.interaction' - TEMPLATEBOT_REPO: "https://github.com/lsst/templates" - TEMPLATEBOT_REPO_REF: "main" - TEMPLATEBOT_ENABLE_SLACK_CONSUMER: "1" - TEMPLATEBOT_ENABLE_EVENTS_CONSUMER: "1" - TEMPLATEBOT_TOPIC_CONFIG: "1" diff --git a/manifests/base/deployment.yaml b/manifests/base/deployment.yaml deleted file mode 100644 index 09c12bd..0000000 --- a/manifests/base/deployment.yaml +++ /dev/null @@ -1,56 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: templatebot - labels: - app: templatebot -spec: - replicas: 1 - selector: - matchLabels: - app: templatebot - template: - metadata: - labels: - app: templatebot - spec: - containers: - - name: templatebot-app - image: lsstsqre/templatebot - imagePullPolicy: Always - ports: - - containerPort: 8080 - name: templatebot-app - env: - - name: SLACK_TOKEN - valueFrom: - secretKeyRef: - name: templatebot - key: SLACK_TOKEN - - name: TEMPLATEBOT_GITHUB_TOKEN - valueFrom: - secretKeyRef: - name: templatebot - key: TEMPLATEBOT_GITHUB_TOKEN - - name: TEMPLATEBOT_GITHUB_USER - valueFrom: - secretKeyRef: - name: templatebot - key: TEMPLATEBOT_GITHUB_USER - - name: TEMPLATEBOT_CACHE_PATH - value: "/etc/templatebot-repo-cache" - - name: TEMPLATEBOT_CERT_CACHE - value: "/etc/templatebot-cert-cache" - envFrom: - - configMapRef: - name: templatebot - volumeMounts: - - name: "repo-cache" - mountPath: "/etc/templatebot-repo-cache" - - name: "cert-cache" - mountPath: "/etc/templatebot-cert-cache" - volumes: - - name: "repo-cache" - emptyDir: {} - - name: "cert-cache" - emptyDir: {} diff --git a/manifests/base/kustomization.yaml b/manifests/base/kustomization.yaml deleted file mode 100644 index 60f68f9..0000000 --- a/manifests/base/kustomization.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - configmap.yaml - - deployment.yaml - -images: - - name: lsstsqre/templatebot - newTag: 0.2.0 diff --git a/manifests/secret.template.yaml b/manifests/secret.template.yaml deleted file mode 100644 index 9310b4d..0000000 --- a/manifests/secret.template.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Credentials for templatebot app. -apiVersion: v1 -kind: Secret -metadata: - name: templatebot -type: Opaque -data: - # echo -n '...' | base64 - TEMPLATEBOT_GITHUB_TOKEN: "{{ TEMPLATEBOT_GITHUB_TOKEN }}" - TEMPLATEBOT_GITHUB_USER: "{{ TEMPALTEBOT_GITHUB_USER }}" - SLACK_TOKEN: "{{ SLACK_TOKEN }}" diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..13aa12b --- /dev/null +++ b/noxfile.py @@ -0,0 +1,190 @@ +import nox + +# Default sessions +nox.options.sessions = ["lint", "typing", "test"] + +# Other nox defaults +nox.options.default_venv_backend = "uv" +nox.options.reuse_existing_virtualenvs = True + + +# Pip installable dependencies +PIP_DEPENDENCIES = [ + ("-r", "requirements/main.txt"), + ("-r", "requirements/dev.txt"), + ("-e", "."), +] + + +def _install(session: nox.Session) -> None: + """Install the application and all dependencies into the session.""" + session.install("--upgrade", "uv") + for deps in PIP_DEPENDENCIES: + session.install(*deps) + + +def _make_env_vars(overrides: dict[str, str] | None = None) -> dict[str, str]: + """Create a environment variable dictionary for test sessions that enables + the app to start up. + """ + env_vars = { + "KAFKA_BOOTSTRAP_SERVERS": "localhost:9092", + "TEMPLATEBOT_PROFILE": "development", + "TEMPLATEBOT_LOG_LEVEL": "DEBUG", + "TEMPLATEBOT_ENVIRONMENT_URL": "http://example.com/", + "TEMPLATEBOT_SLACK_TOKEN": "xoxb-testing-123", + "TEMPLATEBOT_SLACK_APP_ID": "A123456", + "TEMPLATEBOT_TEMPLATE_REPO_URL": "https://github.com/lsst/templates", + "TEMPLATEBOT_TEMPLATE_CACHE_DIR": ".tmp/template_cache", + "TEMPLATEBOT_GITHUB_APP_ID": "1234", + "TEMPLATEBOT_GITHUB_APP_PRIVATE_KEY": "test", + "TEMPLATEBOT_LTD_USERNAME": "test", + "TEMPLATEBOT_LTD_PASSWORD": "test", + } + if overrides: + env_vars.update(overrides) + return env_vars + + +def _install_dev(session: nox.Session, bin_prefix: str = "") -> None: + """Install the application and all development dependencies into the + session. + """ + python = f"{bin_prefix}python" + precommit = f"{bin_prefix}pre-commit" + + # Install dev dependencies + session.run(python, "-m", "pip", "install", "uv", external=True) + for deps in PIP_DEPENDENCIES: + session.run(python, "-m", "uv", "pip", "install", *deps, external=True) + session.run( + python, + "-m", + "uv", + "pip", + "install", + "nox", + "pre-commit", + external=True, + ) + # Install pre-commit hooks + session.run(precommit, "install", external=True) + + +@nox.session(name="venv-init") +def init_dev(session: nox.Session) -> None: + """Set up a development venv.""" + # Create a venv in the current directory, replacing any existing one + session.run("python", "-m", "venv", ".venv", "--clear") + _install_dev(session, bin_prefix=".venv/bin/") + + print( + "\nTo activate this virtual env, run:\n\n\tsource .venv/bin/activate\n" + ) + + +@nox.session(name="init", venv_backend=None, python=False) +def init(session: nox.Session) -> None: + """Set up the development environment in the current virtual env.""" + _install_dev(session, bin_prefix="") + + +@nox.session +def lint(session: nox.Session) -> None: + """Run pre-commit hooks.""" + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files", *session.posargs) + + +@nox.session +def typing(session: nox.Session) -> None: + """Run mypy.""" + _install(session) + session.install("mypy") + session.run("mypy", "noxfile.py", "src", "tests") + + +@nox.session +def test(session: nox.Session) -> None: + """Run pytest.""" + from testcontainers.kafka import KafkaContainer + + _install(session) + + with KafkaContainer().with_kraft() as kafka: + session.run( + "pytest", + "--cov=templatebot", + "--cov-branch", + *session.posargs, + env=_make_env_vars( + {"KAFKA_BOOTSTRAP_SERVERS": kafka.get_bootstrap_server()} + ), + ) + + +@nox.session(name="scriv-create") +def scriv_create(session: nox.Session) -> None: + """Create a scriv entry.""" + session.install("scriv") + session.run("scriv", "create") + + +@nox.session(name="scriv-collect") +def scriv_collect(session: nox.Session) -> None: + """Collect scriv entries.""" + session.install("scriv") + session.run("scriv", "collect", "--add", "--version", *session.posargs) + + +@nox.session(name="update-deps") +def update_deps(session: nox.Session) -> None: + """Update pinned server dependencies and pre-commit hooks.""" + session.install("--upgrade", "uv", "pre-commit") + session.run("pre-commit", "autoupdate") + + # Dependencies are unpinned for compatibility with the unpinned client + # dependency. + session.run( + "uv", + "pip", + "compile", + "--upgrade", + "--universal", + "--generate-hashes", + "--output-file", + "requirements/main.txt", + "requirements/main.in", + ) + + session.run( + "uv", + "pip", + "compile", + "--upgrade", + "--universal", + "--generate-hashes", + "--output-file", + "requirements/dev.txt", + "requirements/dev.in", + ) + + print("\nTo refresh the development venv, run:\n\n\tnox -s init\n") + + +@nox.session(name="run") +def run(session: nox.Session) -> None: + """Run the application in development mode.""" + _install(session) + + from testcontainers.kafka import KafkaContainer + + with KafkaContainer().with_kraft() as kafka: + session.run( + "uvicorn", + "templatebot.main:app", + "--reload", + env=_make_env_vars( + {"KAFKA_BOOTSTRAP_SERVERS": kafka.get_bootstrap_server()} + ), + ) diff --git a/src/templatebot/events/__init__.py b/original/events/__init__.py similarity index 100% rename from src/templatebot/events/__init__.py rename to original/events/__init__.py diff --git a/src/templatebot/events/handlers/__init__.py b/original/events/handlers/__init__.py similarity index 100% rename from src/templatebot/events/handlers/__init__.py rename to original/events/handlers/__init__.py diff --git a/src/templatebot/events/handlers/projectrender.py b/original/events/handlers/projectrender.py similarity index 99% rename from src/templatebot/events/handlers/projectrender.py rename to original/events/handlers/projectrender.py index ef0b72b..c967108 100644 --- a/src/templatebot/events/handlers/projectrender.py +++ b/original/events/handlers/projectrender.py @@ -143,7 +143,7 @@ async def handle_project_render(*, event, schema, app, logger): # First, copy and reset the event based on render_ready postrender_payload = copy.deepcopy(event) postrender_payload["retry_count"] = 0 - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) postrender_payload["initial_timestamp"] = now serializer = app["templatebot/eventSerializer"] diff --git a/src/templatebot/events/schemas/templatebot.postrender_v1.json b/original/events/schemas/templatebot.postrender_v1.json similarity index 100% rename from src/templatebot/events/schemas/templatebot.postrender_v1.json rename to original/events/schemas/templatebot.postrender_v1.json diff --git a/src/templatebot/events/schemas/templatebot.prerender_v1.json b/original/events/schemas/templatebot.prerender_v1.json similarity index 100% rename from src/templatebot/events/schemas/templatebot.prerender_v1.json rename to original/events/schemas/templatebot.prerender_v1.json diff --git a/src/templatebot/events/schemas/templatebot.render_ready_v1.json b/original/events/schemas/templatebot.render_ready_v1.json similarity index 100% rename from src/templatebot/events/schemas/templatebot.render_ready_v1.json rename to original/events/schemas/templatebot.render_ready_v1.json diff --git a/src/templatebot/slack/__init__.py b/original/slack/__init__.py similarity index 100% rename from src/templatebot/slack/__init__.py rename to original/slack/__init__.py diff --git a/src/templatebot/slack/chat.py b/original/slack/chat.py similarity index 100% rename from src/templatebot/slack/chat.py rename to original/slack/chat.py diff --git a/src/templatebot/slack/dialog.py b/original/slack/dialog.py similarity index 99% rename from src/templatebot/slack/dialog.py rename to original/slack/dialog.py index 413e728..ee342fb 100644 --- a/src/templatebot/slack/dialog.py +++ b/original/slack/dialog.py @@ -52,7 +52,7 @@ async def open_template_dialog( "trigger_id": event_data["trigger_id"], "dialog": { "title": dialog_title, - "callback_id": f"{callback_id_root}_{str(uuid.uuid4())}", + "callback_id": f"{callback_id_root}_{uuid.uuid4()!s}", "state": json.dumps(state), "notify_on_cancel": True, "elements": elements, @@ -221,7 +221,6 @@ def post_process_dialog_submission(*, submission_data, template): data = {k: v for k, v in submission_data.items() if v is not None} for field in template.config["dialog_fields"]: - if "preset_groups" in field: # Handle as a preset_groups select menu selected_label = data[field["label"]] diff --git a/src/templatebot/slack/handlers/__init__.py b/original/slack/handlers/__init__.py similarity index 100% rename from src/templatebot/slack/handlers/__init__.py rename to original/slack/handlers/__init__.py diff --git a/src/templatebot/slack/handlers/filedialogsubmission.py b/original/slack/handlers/filedialogsubmission.py similarity index 98% rename from src/templatebot/slack/handlers/filedialogsubmission.py rename to original/slack/handlers/filedialogsubmission.py index 7d3fc7b..6884cec 100644 --- a/src/templatebot/slack/handlers/filedialogsubmission.py +++ b/original/slack/handlers/filedialogsubmission.py @@ -1,5 +1,4 @@ -"""Slack handler when a user submits a dialog to create a file. -""" +"""Slack handler when a user submits a dialog to create a file.""" import json import os.path diff --git a/src/templatebot/slack/handlers/filelisting.py b/original/slack/handlers/filelisting.py similarity index 100% rename from src/templatebot/slack/handlers/filelisting.py rename to original/slack/handlers/filelisting.py diff --git a/src/templatebot/slack/handlers/fileselect.py b/original/slack/handlers/fileselect.py similarity index 100% rename from src/templatebot/slack/handlers/fileselect.py rename to original/slack/handlers/fileselect.py diff --git a/src/templatebot/slack/handlers/help.py b/original/slack/handlers/help.py similarity index 100% rename from src/templatebot/slack/handlers/help.py rename to original/slack/handlers/help.py diff --git a/src/templatebot/slack/handlers/projectdialogsubmission.py b/original/slack/handlers/projectdialogsubmission.py similarity index 97% rename from src/templatebot/slack/handlers/projectdialogsubmission.py rename to original/slack/handlers/projectdialogsubmission.py index 72e77df..79d4668 100644 --- a/src/templatebot/slack/handlers/projectdialogsubmission.py +++ b/original/slack/handlers/projectdialogsubmission.py @@ -74,7 +74,7 @@ async def handle_project_dialog_submission(*, event_data, logger, app): "template_repo": app["root"]["templatebot/repoUrl"], "template_repo_ref": app["root"]["templatebot/repoRef"], "retry_count": 0, - "initial_timestamp": datetime.datetime.now(datetime.timezone.utc), + "initial_timestamp": datetime.datetime.now(datetime.UTC), "slack_username": user_id, "slack_channel": channel_id, "slack_thread_ts": slack_thread_ts, diff --git a/src/templatebot/slack/handlers/projectlisting.py b/original/slack/handlers/projectlisting.py similarity index 100% rename from src/templatebot/slack/handlers/projectlisting.py rename to original/slack/handlers/projectlisting.py diff --git a/src/templatebot/slack/handlers/projectselect.py b/original/slack/handlers/projectselect.py similarity index 100% rename from src/templatebot/slack/handlers/projectselect.py rename to original/slack/handlers/projectselect.py diff --git a/src/templatebot/slack/router.py b/original/slack/router.py similarity index 50% rename from src/templatebot/slack/router.py rename to original/slack/router.py index db47d82..7bdea97 100644 --- a/src/templatebot/slack/router.py +++ b/original/slack/router.py @@ -1,12 +1,8 @@ """Route incoming Slack messages from SQuaRE Events to handlers.""" -import asyncio import re import structlog -from aiokafka import AIOKafkaConsumer -from kafkit.registry import Deserializer -from kafkit.registry.aiohttp import RegistryApi from .handlers import ( handle_file_creation, @@ -18,108 +14,9 @@ handle_project_select_action, ) -__all__ = ["consume_kafka"] - MENTION_PATTERN = re.compile(r"<(@[a-zA-Z0-9]+|!subteam\^[a-zA-Z0-9]+)>") -async def consume_kafka(app): - """Consume Kafka messages directed to templatebot's functionality.""" - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - - registry = RegistryApi( - session=app["root"]["api.lsst.codes/httpSession"], - url=app["root"]["templatebot/registryUrl"], - ) - deserializer = Deserializer(registry=registry) - - consumer_settings = { - "bootstrap_servers": app["root"]["templatebot/brokerUrl"], - "group_id": app["root"]["templatebot/slackGroupId"], - "auto_offset_reset": "latest", - "ssl_context": app["root"]["templatebot/kafkaSslContext"], - "security_protocol": app["root"]["templatebot/kafkaProtocol"], - } - consumer = AIOKafkaConsumer( - loop=asyncio.get_event_loop(), **consumer_settings - ) - - try: - await consumer.start() - logger.info("Started Kafka consumer", **consumer_settings) - - topic_names = [ - app["root"]["templatebot/appMentionTopic"], - app["root"]["templatebot/messageImTopic"], - app["root"]["templatebot/interactionTopic"], - ] - logger.info("Subscribing to Kafka topics", names=topic_names) - consumer.subscribe(topic_names) - - logger.info("Finished subscribing ot Kafka topics", names=topic_names) - - partitions = consumer.assignment() - logger.info("Waiting on partition assignment", names=topic_names) - while len(partitions) == 0: - # Wait for the consumer to get partition assignment - await asyncio.sleep(1.0) - partitions = consumer.assignment() - logger.info( - "Initial partition assignment", - partitions=[str(p) for p in partitions], - ) - - async for message in consumer: - logger.info( - "Got Kafka message from sqrbot", - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - try: - message_info = await deserializer.deserialize(message.value) - except Exception: - logger.exception( - "Failed to deserialize a message", - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - continue - - event = message_info["message"] - logger.debug( - "New message", - topic=message.topic, - partition=message.partition, - offset=message.offset, - contents=event, - ) - - try: - await route_event( - event=message_info["message"], - app=app, - schema_id=message_info["id"], - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - except Exception: - logger.exception( - "Failed to handle message", - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - - except asyncio.CancelledError: - logger.info("consume_kafka task got cancelled") - finally: - logger.info("consume_kafka task cancelling") - await consumer.stop() - - async def route_event(*, event, schema_id, topic, partition, offset, app): """Route an incoming event, from Kafka, to a handler.""" logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) diff --git a/src/templatebot/slack/users.py b/original/slack/users.py similarity index 100% rename from src/templatebot/slack/users.py rename to original/slack/users.py diff --git a/pyproject.toml b/pyproject.toml index 0fadc78..a557395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,34 @@ -[build-system] -requires = [ - "setuptools>=42", - "wheel", - "setuptools_scm[toml]>=3.4" +[project] +# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ +name = "templatebot" +description = "Templatebot is a Squarebot backend for creating files and repositories from templates." +license = { file = "LICENSE" } +readme = "README.md" +keywords = ["rubin", "lsst"] +# https://pypi.org/classifiers/ +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: POSIX", + "Typing :: Typed", ] -build-backend = 'setuptools.build_meta' +requires-python = ">=3.12" +# Use requirements/main.in for runtime dependencies instead. +dependencies = [] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/lsst-sqre/templatebot" +Source = "https://github.com/lsst-sqre/templatebot" + +[build-system] +requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" [tool.setuptools_scm] @@ -27,29 +51,69 @@ exclude_lines = [ "raise NotImplementedError", "if 0:", "if __name__ == .__main__.:", - "if TYPE_CHECKING:" + "if TYPE_CHECKING:", +] + +[tool.mypy] +disallow_untyped_defs = true +disallow_incomplete_defs = true +ignore_missing_imports = true +local_partial_types = true +plugins = ["pydantic.mypy"] +no_implicit_reexport = true +show_error_codes = true +strict_equality = true +warn_redundant_casts = true +warn_unreachable = true +warn_unused_ignores = true + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true +warn_untyped_fields = true + +[tool.pytest.ini_options] +asyncio_mode = "strict" +# The python_files setting is not for test detection (pytest will pick up any +# test files named *_test.py without this setting) but to enable special +# assert processing in any non-test supporting files under tests. We +# conventionally put test support functions under tests.support and may +# sometimes use assert in test fixtures in conftest.py, and pytest only +# enables magical assert processing (showing a full diff on assert failures +# with complex data structures rather than only the assert message) in files +# listed in python_files. +python_files = ["tests/*.py", "tests/*/*.py"] + +# The rule used with Ruff configuration is to disable every lint that has +# legitimate exceptions that are not dodgy code, rather than cluttering code +# with noqa markers. This is therefore a reiatively relaxed configuration that +# errs on the side of disabling legitimate lints. +# +# Reference for settings: https://beta.ruff.rs/docs/settings/ +# Reference for rules: https://beta.ruff.rs/docs/rules/ +[tool.ruff] +extend = "ruff-shared.toml" +extend-exclude = ["original/**"] + +[tool.ruff.lint.extend-per-file-ignores] +"noxfile.py" = [ + "D100", # No docstring for module ] -[tool.black] -line-length = 79 -target-version = ['py38'] -exclude = ''' -/( - \.eggs - | \.git - | \.mypy_cache - | \.tox - | \.venv - | _build - | build - | dist -)/ -''' -# Use single-quoted strings so TOML treats the string like a Python r-string -# Multi-line strings are implicitly treated by black as regular expressions - -[tool.isort] -include_trailing_comma = true -multi_line_output = 3 -known_first_party = ["templatebot", "tests"] -skip = ["docs/conf.py"] +[tool.ruff.lint.isort] +known-first-party = ["templatebot", "tests"] +split-on-trailing-comma = false + +[tool.scriv] +categories = [ + "Backwards-incompatible changes", + "New features", + "Bug fixes", + "Other changes", +] +entry_title_template = "{{ version }} ({{ date.strftime('%Y-%m-%d') }})" +format = "md" +md_header_level = "2" +new_fragment_template = "file:changelog.d/_template.md.jinja" +skip_fragments = "_template.md.jinja" diff --git a/requirements/dev.in b/requirements/dev.in index a4bcc60..2185e31 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -1,3 +1,5 @@ +# -*- conf -*- +# # Editable development dependencies # Add direct development, test, and documentation dependencies here, as well # as implicit dev dependencies with constrained versions. @@ -7,14 +9,19 @@ -c main.txt -# Testing and linting +# Type stubs +types-PyYAML + +# Testing +asgi-lifespan +coverage[toml] +httpx mypy +pydantic pytest -pytest-coverage -pytest-aiohttp - -# Development server -aiohttp-devtools +pytest-asyncio +pytest-cov +testcontainers[kafka] # Documentation -documenteer[pipelines] +scriv diff --git a/requirements/dev.txt b/requirements/dev.txt index b93e9c9..c7abdfd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,768 +1,624 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile --generate-hashes --output-file=requirements/dev.txt requirements/dev.in -# -aiohttp==3.8.1 \ - --hash=sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3 \ - --hash=sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782 \ - --hash=sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75 \ - --hash=sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf \ - --hash=sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7 \ - --hash=sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675 \ - --hash=sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1 \ - --hash=sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785 \ - --hash=sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4 \ - --hash=sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf \ - --hash=sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5 \ - --hash=sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15 \ - --hash=sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca \ - --hash=sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8 \ - --hash=sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac \ - --hash=sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8 \ - --hash=sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef \ - --hash=sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516 \ - --hash=sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700 \ - --hash=sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2 \ - --hash=sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8 \ - --hash=sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0 \ - --hash=sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676 \ - --hash=sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad \ - --hash=sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155 \ - --hash=sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db \ - --hash=sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd \ - --hash=sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091 \ - --hash=sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602 \ - --hash=sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411 \ - --hash=sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93 \ - --hash=sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd \ - --hash=sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec \ - --hash=sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51 \ - --hash=sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7 \ - --hash=sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17 \ - --hash=sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d \ - --hash=sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00 \ - --hash=sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923 \ - --hash=sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440 \ - --hash=sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32 \ - --hash=sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e \ - --hash=sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1 \ - --hash=sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724 \ - --hash=sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a \ - --hash=sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8 \ - --hash=sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2 \ - --hash=sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33 \ - --hash=sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b \ - --hash=sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2 \ - --hash=sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632 \ - --hash=sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b \ - --hash=sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2 \ - --hash=sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316 \ - --hash=sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74 \ - --hash=sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96 \ - --hash=sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866 \ - --hash=sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44 \ - --hash=sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950 \ - --hash=sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa \ - --hash=sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c \ - --hash=sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a \ - --hash=sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd \ - --hash=sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd \ - --hash=sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9 \ - --hash=sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421 \ - --hash=sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2 \ - --hash=sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922 \ - --hash=sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4 \ - --hash=sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237 \ - --hash=sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642 \ - --hash=sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578 +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --output-file requirements/dev.txt requirements/dev.in +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via # -c requirements/main.txt - # aiohttp-devtools - # pytest-aiohttp -aiohttp-devtools==1.0.post0 \ - --hash=sha256:1847ce92e6e8ca1ed5dc603864f4faecdfa40b6b5aeb5193f05cafd28d3ce3e3 \ - --hash=sha256:f46a87b250a84bf8edca21c8b6991dc2b6145cdf0af8a08b73f5c92d48d85f01 - # via -r requirements/dev.in -aiosignal==1.2.0 \ - --hash=sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a \ - --hash=sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2 - # via - # -c requirements/main.txt - # aiohttp -alabaster==0.7.12 \ - --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ - --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 - # via sphinx -anyio==3.6.1 \ - --hash=sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b \ - --hash=sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be - # via watchgod -asttokens==2.0.5 \ - --hash=sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c \ - --hash=sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5 - # via devtools -async-timeout==4.0.2 \ - --hash=sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15 \ - --hash=sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c + # pydantic +anyio==4.6.0 \ + --hash=sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb \ + --hash=sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a # via # -c requirements/main.txt - # aiohttp -attrs==21.4.0 \ - --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ - --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd + # httpx +asgi-lifespan==2.1.0 \ + --hash=sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308 \ + --hash=sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f + # via -r requirements/dev.in +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 + # via scriv +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 # via # -c requirements/main.txt - # aiohttp - # pytest -babel==2.10.3 \ - --hash=sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51 \ - --hash=sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb - # via sphinx -certifi==2022.6.15 \ - --hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \ - --hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412 + # httpcore + # httpx + # requests +charset-normalizer==3.3.2 \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 # via # -c requirements/main.txt # requests -charset-normalizer==2.1.0 \ - --hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \ - --hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413 +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via # -c requirements/main.txt - # aiohttp - # requests -click==8.1.3 \ - --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ - --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 + # click-log + # scriv +click-log==0.4.0 \ + --hash=sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975 \ + --hash=sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756 + # via scriv +colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via # -c requirements/main.txt - # aiohttp-devtools - # documenteer - # sphinx-click -coverage[toml]==6.4.2 \ - --hash=sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32 \ - --hash=sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7 \ - --hash=sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996 \ - --hash=sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55 \ - --hash=sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46 \ - --hash=sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de \ - --hash=sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039 \ - --hash=sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee \ - --hash=sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1 \ - --hash=sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f \ - --hash=sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63 \ - --hash=sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083 \ - --hash=sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe \ - --hash=sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0 \ - --hash=sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6 \ - --hash=sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe \ - --hash=sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933 \ - --hash=sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0 \ - --hash=sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c \ - --hash=sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07 \ - --hash=sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8 \ - --hash=sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b \ - --hash=sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e \ - --hash=sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120 \ - --hash=sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f \ - --hash=sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e \ - --hash=sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd \ - --hash=sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f \ - --hash=sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386 \ - --hash=sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8 \ - --hash=sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae \ - --hash=sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc \ - --hash=sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783 \ - --hash=sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d \ - --hash=sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c \ - --hash=sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97 \ - --hash=sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978 \ - --hash=sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf \ - --hash=sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29 \ - --hash=sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39 \ - --hash=sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452 - # via pytest-cov -devtools==0.9.0 \ - --hash=sha256:689cf4e7c75024237c42093ba19f4fa9cf15980269f02463aeab4d97d4b0a215 \ - --hash=sha256:86ede6e0273e023db766344d14098228785b48a80f31716f28e8b9453d52fa1e - # via aiohttp-devtools -documenteer[pipelines]==0.6.12 \ - --hash=sha256:21be14255b72919a280b296313261970b77f3083770978f266aad78ae6a254d5 \ - --hash=sha256:6e746c2c98f4b7e687f4f08043085935af47d9b1c52a783cdf4fa8d02e5bc9e7 - # via -r requirements/dev.in -docutils==0.19 \ - --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ - --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc + # click + # pytest +coverage==7.6.1 \ + --hash=sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca \ + --hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \ + --hash=sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6 \ + --hash=sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989 \ + --hash=sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c \ + --hash=sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b \ + --hash=sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223 \ + --hash=sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f \ + --hash=sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56 \ + --hash=sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3 \ + --hash=sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8 \ + --hash=sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb \ + --hash=sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388 \ + --hash=sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0 \ + --hash=sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a \ + --hash=sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8 \ + --hash=sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f \ + --hash=sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a \ + --hash=sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962 \ + --hash=sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8 \ + --hash=sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391 \ + --hash=sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc \ + --hash=sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2 \ + --hash=sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155 \ + --hash=sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb \ + --hash=sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0 \ + --hash=sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c \ + --hash=sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a \ + --hash=sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004 \ + --hash=sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060 \ + --hash=sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232 \ + --hash=sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93 \ + --hash=sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129 \ + --hash=sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163 \ + --hash=sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de \ + --hash=sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6 \ + --hash=sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23 \ + --hash=sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569 \ + --hash=sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d \ + --hash=sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778 \ + --hash=sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d \ + --hash=sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36 \ + --hash=sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a \ + --hash=sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6 \ + --hash=sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34 \ + --hash=sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704 \ + --hash=sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106 \ + --hash=sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9 \ + --hash=sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862 \ + --hash=sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b \ + --hash=sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255 \ + --hash=sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16 \ + --hash=sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3 \ + --hash=sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133 \ + --hash=sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb \ + --hash=sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657 \ + --hash=sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d \ + --hash=sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca \ + --hash=sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36 \ + --hash=sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c \ + --hash=sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e \ + --hash=sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff \ + --hash=sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7 \ + --hash=sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5 \ + --hash=sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02 \ + --hash=sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c \ + --hash=sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df \ + --hash=sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3 \ + --hash=sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a \ + --hash=sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959 \ + --hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \ + --hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc # via - # pybtex-docutils - # sphinx - # sphinx-click - # sphinx-jinja - # sphinxcontrib-bibtex -executing==0.9.1 \ - --hash=sha256:4ce4d6082d99361c0231fc31ac1a0f56979363cc6819de0b1410784f99e49105 \ - --hash=sha256:ea278e2cf90cbbacd24f1080dd1f0ac25b71b2e21f50ab439b7ba45dd3195587 - # via devtools -frozenlist==1.3.0 \ - --hash=sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e \ - --hash=sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08 \ - --hash=sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b \ - --hash=sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486 \ - --hash=sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78 \ - --hash=sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468 \ - --hash=sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1 \ - --hash=sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953 \ - --hash=sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3 \ - --hash=sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d \ - --hash=sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a \ - --hash=sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141 \ - --hash=sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08 \ - --hash=sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07 \ - --hash=sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa \ - --hash=sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa \ - --hash=sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868 \ - --hash=sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f \ - --hash=sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b \ - --hash=sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b \ - --hash=sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1 \ - --hash=sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f \ - --hash=sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478 \ - --hash=sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58 \ - --hash=sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01 \ - --hash=sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8 \ - --hash=sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d \ - --hash=sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676 \ - --hash=sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274 \ - --hash=sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab \ - --hash=sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8 \ - --hash=sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24 \ - --hash=sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a \ - --hash=sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2 \ - --hash=sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f \ - --hash=sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f \ - --hash=sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93 \ - --hash=sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1 \ - --hash=sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51 \ - --hash=sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846 \ - --hash=sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5 \ - --hash=sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d \ - --hash=sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c \ - --hash=sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e \ - --hash=sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae \ - --hash=sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02 \ - --hash=sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0 \ - --hash=sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b \ - --hash=sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3 \ - --hash=sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b \ - --hash=sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa \ - --hash=sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a \ - --hash=sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d \ - --hash=sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed \ - --hash=sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148 \ - --hash=sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9 \ - --hash=sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c \ - --hash=sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2 \ - --hash=sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951 + # -r requirements/dev.in + # pytest-cov +docker==7.1.0 \ + --hash=sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c \ + --hash=sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 + # via testcontainers +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 # via # -c requirements/main.txt - # aiohttp - # aiosignal -gitdb==4.0.9 \ - --hash=sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd \ - --hash=sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa + # httpcore +httpcore==1.0.6 \ + --hash=sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f \ + --hash=sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f # via # -c requirements/main.txt - # gitpython -gitpython==3.1.27 \ - --hash=sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704 \ - --hash=sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d + # httpx +httpx==0.27.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 # via # -c requirements/main.txt - # documenteer -idna==3.3 \ - --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ - --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d + # -r requirements/dev.in +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 # via # -c requirements/main.txt # anyio + # httpx # requests - # yarl -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a - # via sphinx -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 - # via sphinx -iniconfig==1.1.1 \ - --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ - --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # via pytest -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # via # -c requirements/main.txt - # numpydoc - # sphinx - # sphinx-jinja -latexcodec==2.0.1 \ - --hash=sha256:2aa2551c373261cefe2ad3a8953a6d6533e68238d180eb4bb91d7964adb3fe9a \ - --hash=sha256:c277a193638dc7683c4c30f6684e3db728a06efb0dc9cf346db8bd0aa6c5d271 - # via pybtex -lsst-sphinx-bootstrap-theme==0.2.2 \ - --hash=sha256:71c14b309cac280301facdd6db4fc43be9b95962e20c16bb84678fd813f2d2a1 - # via documenteer -markupsafe==2.1.1 \ - --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \ - --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \ - --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \ - --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \ - --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \ - --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \ - --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \ - --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \ - --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \ - --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \ - --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \ - --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \ - --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \ - --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \ - --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \ - --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \ - --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \ - --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \ - --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \ - --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \ - --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \ - --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \ - --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \ - --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \ - --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \ - --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \ - --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \ - --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \ - --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \ - --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \ - --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \ - --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \ - --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \ - --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \ - --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \ - --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \ - --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \ - --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \ - --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \ - --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7 + # scriv +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via scriv +markupsafe==3.0.0 \ + --hash=sha256:03ff62dea2fef3eadf2f1853bc6332bcb0458d9608b11dfb1cd5aeda1c178ea6 \ + --hash=sha256:105ada43a61af22acb8774514c51900dc820c481cc5ba53f17c09d294d9c07ca \ + --hash=sha256:12ddac720b8965332d36196f6f83477c6351ba6a25d4aff91e30708c729350d7 \ + --hash=sha256:1d151b9cf3307e259b749125a5a08c030ba15a8f1d567ca5bfb0e92f35e761f5 \ + --hash=sha256:1ee9790be6f62121c4c58bbced387b0965ab7bffeecb4e17cc42ef290784e363 \ + --hash=sha256:1fd02f47596e00a372f5b4af2b4c45f528bade65c66dfcbc6e1ea1bfda758e98 \ + --hash=sha256:23efb2be7221105c8eb0e905433414d2439cb0a8c5d5ca081c1c72acef0f5613 \ + --hash=sha256:25396abd52b16900932e05b7104bcdc640a4d96c914f39c3b984e5a17b01fba0 \ + --hash=sha256:27d6a73682b99568916c54a4bfced40e7d871ba685b580ea04bbd2e405dfd4c5 \ + --hash=sha256:380faf314c3c84c1682ca672e6280c6c59e92d0bc13dc71758ffa2de3cd4e252 \ + --hash=sha256:3b231255770723f1e125d63c14269bcd8b8136ecfb620b9a18c0297e046d0736 \ + --hash=sha256:3cd0bba31d484fe9b9d77698ddb67c978704603dc10cdc905512af308cfcca6b \ + --hash=sha256:3efde9a8c56c3b6e5f3fa4baea828f8184970c7c78480fedb620d804b1c31e5c \ + --hash=sha256:409535e0521c4630d5b5a1bf284e9d3c76d2fc2f153ebb12cf3827797798cc99 \ + --hash=sha256:494a64efc535e147fcc713dba58eecfce3a79f1e93ebe81995b387f5cd9bc2e1 \ + --hash=sha256:4ca04c60006867610a06575b46941ae616b19da0adc85b9f8f3d9cbd7a3da385 \ + --hash=sha256:4deea1d9169578917d1f35cdb581bc7bab56a7e8c5be2633bd1b9549c3c22a01 \ + --hash=sha256:509c424069dd037d078925b6815fc56b7271f3aaec471e55e6fa513b0a80d2aa \ + --hash=sha256:5509a8373fed30b978557890a226c3d30569746c565b9daba69df80c160365a5 \ + --hash=sha256:59420b5a9a5d3fee483a32adb56d7369ae0d630798da056001be1e9f674f3aa6 \ + --hash=sha256:5d207ff5cceef77796f8aacd44263266248cf1fbc601441524d7835613f8abec \ + --hash=sha256:5ddf5cb8e9c00d9bf8b0c75949fb3ff9ea2096ba531693e2e87336d197fdb908 \ + --hash=sha256:63dae84964a9a3d2610808cee038f435d9a111620c37ccf872c2fcaeca6865b3 \ + --hash=sha256:64a7c7856c3a409011139b17d137c2924df4318dab91ee0530800819617c4381 \ + --hash=sha256:64f7d04410be600aa5ec0626d73d43e68a51c86500ce12917e10fd013e258df5 \ + --hash=sha256:658fdf6022740896c403d45148bf0c36978c6b48c9ef8b1f8d0c7a11b6cdea86 \ + --hash=sha256:678fbceb202382aae42c1f0cd9f56b776bc20a58ae5b553ee1fe6b802983a1d6 \ + --hash=sha256:7835de4c56066e096407a1852e5561f6033786dd987fa90dc384e45b9bd21295 \ + --hash=sha256:7c524203207f5b569df06c96dafdc337228921ee8c3cc5f6e891d024c6595352 \ + --hash=sha256:7ed789d0f7f11fcf118cf0acb378743dfdd4215d7f7d18837c88171405c9a452 \ + --hash=sha256:81be2c0084d8c69e97e3c5d73ce9e2a6e523556f2a19c4e195c09d499be2f808 \ + --hash=sha256:81ee9c967956b9ea39b3a5270b7cb1740928d205b0dc72629164ce621b4debf9 \ + --hash=sha256:8219e2207f6c188d15614ea043636c2b36d2d79bf853639c124a179412325a13 \ + --hash=sha256:96e3ed550600185d34429477f1176cedea8293fa40e47fe37a05751bcb64c997 \ + --hash=sha256:98fb3a2bf525ad66db96745707b93ba0f78928b7a1cb2f1cb4b143bc7e2ba3b3 \ + --hash=sha256:9b36473a2d3e882d1873ea906ce54408b9588dc2c65989664e6e7f5a2de353d7 \ + --hash=sha256:9f91c90f8f3bf436f81c12eeb4d79f9ddd263c71125e6ad71341906832a34386 \ + --hash=sha256:a5fd5500d4e4f7cc88d8c0f2e45126c4307ed31e08f8ec521474f2fd99d35ac3 \ + --hash=sha256:a7171d2b869e9be238ea318c196baf58fbf272704e9c1cd4be8c380eea963342 \ + --hash=sha256:a80c6740e1bfbe50cea7cbf74f48823bb57bd59d914ee22ff8a81963b08e62d2 \ + --hash=sha256:b2a7afd24d408b907672015555bc10be2382e6c5f62a488e2d452da670bbd389 \ + --hash=sha256:b43ac1eb9f91e0c14aac1d2ef0f76bc7b9ceea51de47536f61268191adf52ad7 \ + --hash=sha256:b6cc46a27d904c9be5732029769acf4b0af69345172ed1ef6d4db0c023ff603b \ + --hash=sha256:b94bec9eda10111ec7102ef909eca4f3c2df979643924bfe58375f560713a7d1 \ + --hash=sha256:bd9b8e458e2bab52f9ad3ab5dc8b689a3c84b12b2a2f64cd9a0dfe209fb6b42f \ + --hash=sha256:c182d45600556917f811aa019d834a89fe4b6f6255da2fd0bdcf80e970f95918 \ + --hash=sha256:c409691696bec2b5e5c9efd9593c99025bf2f317380bf0d993ee0213516d908a \ + --hash=sha256:c5243044a927e8a6bb28517838662a019cd7f73d7f106bbb37ab5e7fa8451a92 \ + --hash=sha256:c8ab7efeff1884c5da8e18f743b667215300e09043820d11723718de0b7db934 \ + --hash=sha256:cb244adf2499aa37d5dc43431990c7f0b632d841af66a51d22bd89c437b60264 \ + --hash=sha256:d261ec38b8a99a39b62e0119ed47fe3b62f7691c500bc1e815265adc016438c1 \ + --hash=sha256:d2c099be5274847d606574234e494f23a359e829ba337ea9037c3a72b0851942 \ + --hash=sha256:d7e63d1977d3806ce0a1a3e0099b089f61abdede5238ca6a3f3bf8877b46d095 \ + --hash=sha256:dba0f83119b9514bc37272ad012f0cc03f0805cc6a2bea7244e19250ac8ff29f \ + --hash=sha256:dcbee57fedc9b2182c54ffc1c5eed316c3da8bbfeda8009e1b5d7220199d15da \ + --hash=sha256:e042ccf8fe5bf8b6a4b38b3f7d618eb10ea20402b0c9f4add9293408de447974 \ + --hash=sha256:e363440c8534bf2f2ef1b8fdc02037eb5fff8fce2a558519b22d6a3a38b3ec5e \ + --hash=sha256:e64b390a306f9e849ee809f92af6a52cda41741c914358e0e9f8499d03741526 \ + --hash=sha256:f0411641d31aa6f7f0cc13f0f18b63b8dc08da5f3a7505972a42ab059f479ba3 \ + --hash=sha256:f1c13c6c908811f867a8e9e66efb2d6c03d1cdd83e92788fe97f693c457dc44f \ + --hash=sha256:f846fd7c241e5bd4161e2a483663eb66e4d8e12130fcdc052f310f388f1d61c6 # via # -c requirements/main.txt # jinja2 -multidict==6.0.2 \ - --hash=sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60 \ - --hash=sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c \ - --hash=sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672 \ - --hash=sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51 \ - --hash=sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032 \ - --hash=sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2 \ - --hash=sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b \ - --hash=sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80 \ - --hash=sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88 \ - --hash=sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a \ - --hash=sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d \ - --hash=sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389 \ - --hash=sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c \ - --hash=sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9 \ - --hash=sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c \ - --hash=sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516 \ - --hash=sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b \ - --hash=sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43 \ - --hash=sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee \ - --hash=sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227 \ - --hash=sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d \ - --hash=sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae \ - --hash=sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7 \ - --hash=sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4 \ - --hash=sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9 \ - --hash=sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f \ - --hash=sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013 \ - --hash=sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9 \ - --hash=sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e \ - --hash=sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693 \ - --hash=sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a \ - --hash=sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15 \ - --hash=sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb \ - --hash=sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96 \ - --hash=sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87 \ - --hash=sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376 \ - --hash=sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658 \ - --hash=sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0 \ - --hash=sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071 \ - --hash=sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360 \ - --hash=sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc \ - --hash=sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3 \ - --hash=sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba \ - --hash=sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8 \ - --hash=sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9 \ - --hash=sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2 \ - --hash=sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3 \ - --hash=sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68 \ - --hash=sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8 \ - --hash=sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d \ - --hash=sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49 \ - --hash=sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608 \ - --hash=sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57 \ - --hash=sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86 \ - --hash=sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20 \ - --hash=sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293 \ - --hash=sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849 \ - --hash=sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937 \ - --hash=sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d - # via - # -c requirements/main.txt - # aiohttp - # yarl -mypy==0.971 \ - --hash=sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655 \ - --hash=sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9 \ - --hash=sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3 \ - --hash=sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6 \ - --hash=sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0 \ - --hash=sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58 \ - --hash=sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103 \ - --hash=sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09 \ - --hash=sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417 \ - --hash=sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56 \ - --hash=sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2 \ - --hash=sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856 \ - --hash=sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0 \ - --hash=sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8 \ - --hash=sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27 \ - --hash=sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5 \ - --hash=sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71 \ - --hash=sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27 \ - --hash=sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe \ - --hash=sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca \ - --hash=sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf \ - --hash=sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9 \ - --hash=sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mypy==1.11.2 \ + --hash=sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36 \ + --hash=sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce \ + --hash=sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6 \ + --hash=sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b \ + --hash=sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca \ + --hash=sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24 \ + --hash=sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383 \ + --hash=sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7 \ + --hash=sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86 \ + --hash=sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d \ + --hash=sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4 \ + --hash=sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8 \ + --hash=sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987 \ + --hash=sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385 \ + --hash=sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79 \ + --hash=sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef \ + --hash=sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6 \ + --hash=sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70 \ + --hash=sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca \ + --hash=sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70 \ + --hash=sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12 \ + --hash=sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104 \ + --hash=sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a \ + --hash=sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318 \ + --hash=sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1 \ + --hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \ + --hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d # via -r requirements/dev.in -mypy-extensions==0.4.3 \ - --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ - --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 +mypy-extensions==1.0.0 \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 # via mypy -numpydoc==1.4.0 \ - --hash=sha256:9494daf1c7612f59905fa09e65c9b8a90bbacb3804d91f7a94e778831e6fcfa5 \ - --hash=sha256:fd26258868ebcc75c816fe68e1d41e3b55bd410941acfb969dee3eef6e5cf260 - # via documenteer -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via + # -c requirements/main.txt # pytest - # sphinx -pluggy==1.0.0 \ - --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ - --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # via pytest -py==1.11.0 \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via pytest -pybtex==0.24.0 \ - --hash=sha256:818eae35b61733e5c007c3fcd2cfb75ed1bc8b4173c1f70b56cc4c0802d34755 \ - --hash=sha256:e1e0c8c69998452fea90e9179aa2a98ab103f3eed894405b7264e517cc2fcc0f - # via - # pybtex-docutils - # sphinxcontrib-bibtex -pybtex-docutils==1.0.2 \ - --hash=sha256:43aa353b6d498fd5ac30f0073a98e332d061d34fe619d3d50d1761f8fd4aa016 \ - --hash=sha256:6f9e3c25a37bcaac8c4f69513272706ec6253bb708a93d8b4b173f43915ba239 - # via sphinxcontrib-bibtex -pygments==2.12.0 \ - --hash=sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb \ - --hash=sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519 +pydantic==2.9.2 \ + --hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \ + --hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12 # via - # aiohttp-devtools - # sphinx - # sphinx-prompt -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc + # -c requirements/main.txt + # -r requirements/dev.in +pydantic-core==2.23.4 \ + --hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \ + --hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \ + --hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \ + --hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \ + --hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \ + --hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \ + --hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \ + --hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \ + --hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \ + --hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \ + --hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \ + --hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \ + --hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \ + --hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \ + --hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \ + --hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \ + --hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \ + --hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \ + --hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \ + --hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \ + --hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \ + --hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \ + --hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \ + --hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \ + --hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \ + --hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \ + --hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \ + --hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \ + --hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \ + --hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \ + --hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \ + --hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \ + --hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \ + --hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \ + --hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \ + --hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \ + --hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \ + --hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \ + --hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \ + --hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \ + --hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \ + --hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \ + --hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \ + --hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \ + --hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \ + --hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \ + --hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \ + --hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \ + --hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \ + --hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \ + --hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \ + --hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \ + --hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \ + --hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \ + --hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \ + --hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \ + --hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \ + --hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \ + --hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \ + --hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \ + --hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \ + --hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \ + --hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \ + --hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \ + --hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \ + --hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \ + --hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \ + --hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \ + --hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \ + --hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \ + --hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \ + --hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \ + --hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \ + --hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \ + --hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \ + --hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \ + --hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \ + --hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \ + --hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \ + --hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \ + --hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \ + --hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \ + --hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \ + --hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \ + --hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \ + --hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \ + --hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \ + --hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \ + --hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607 # via - # packaging - # sphinxcontrib-doxylink -pytest==7.1.2 \ - --hash=sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c \ - --hash=sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45 + # -c requirements/main.txt + # pydantic +pytest==8.3.3 \ + --hash=sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181 \ + --hash=sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2 # via # -r requirements/dev.in - # pytest-aiohttp # pytest-asyncio # pytest-cov -pytest-aiohttp==1.0.4 \ - --hash=sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95 \ - --hash=sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4 +pytest-asyncio==0.24.0 \ + --hash=sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b \ + --hash=sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276 # via -r requirements/dev.in -pytest-asyncio==0.19.0 \ - --hash=sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa \ - --hash=sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed - # via pytest-aiohttp -pytest-cov==3.0.0 \ - --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ - --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 - # via pytest-cover -pytest-cover==3.0.0 \ - --hash=sha256:578249955eb3b5f3991209df6e532bb770b647743b7392d3d97698dc02f39ebb \ - --hash=sha256:5bdb6c1cc3dd75583bb7bc2c57f5e1034a1bfcb79d27c71aceb0b16af981dbf4 - # via pytest-coverage -pytest-coverage==0.0 \ - --hash=sha256:db6af2cbd7e458c7c9fd2b4207cee75258243c8a81cad31a7ee8cfad5be93c05 \ - --hash=sha256:dedd084c5e74d8e669355325916dc011539b190355021b037242514dee546368 +pytest-cov==5.0.0 \ + --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \ + --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857 # via -r requirements/dev.in -python-dateutil==2.8.2 \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 - # via - # -c requirements/main.txt - # sphinxcontrib-doxylink -pytz==2022.1 \ - --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ - --hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c - # via babel -pyyaml==6.0 \ - --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ - --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ - --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ - --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ - --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ - --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ - --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ - --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ - --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ - --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ - --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ - --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ - --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ - --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ - --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ - --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ - --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ - --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ - --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ - --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ - --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ - --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ - --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ - --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ - --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ - --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ - --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ - --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ - --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ - --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ - --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ - --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ - --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 +pywin32==307 ; sys_platform == 'win32' \ + --hash=sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347 \ + --hash=sha256:05de55a7c110478dc4b202230e98af5e0720855360d2b31a44bb4e296d795fba \ + --hash=sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815 \ + --hash=sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4 \ + --hash=sha256:13d059fb7f10792542082f5731d5d3d9645320fc38814759313e5ee97c3fac01 \ + --hash=sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d \ + --hash=sha256:5101472f5180c647d4525a0ed289ec723a26231550dbfd369ec19d5faf60e511 \ + --hash=sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6 \ + --hash=sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2 \ + --hash=sha256:7e0b2f93769d450a98ac7a31a087e07b126b6d571e8b4386a5762eb85325270b \ + --hash=sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3 \ + --hash=sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a \ + --hash=sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2 \ + --hash=sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87 \ + --hash=sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5 \ + --hash=sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b \ + --hash=sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398 \ + --hash=sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75 + # via docker +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # -c requirements/main.txt - # documenteer - # pybtex -requests==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 - # via - # -c requirements/main.txt - # documenteer - # sphinx -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # docker + # scriv +scriv==1.5.1 \ + --hash=sha256:30ae9ff8d144f8e0cf394c4e1d379542f1b3823767642955b54ec40dc00b32b6 \ + --hash=sha256:a3adc657733b4124fcb54527a5f3daab0d3c300de82d0fd2b9b297b243151b78 + # via -r requirements/dev.in +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via # -c requirements/main.txt - # asttokens - # latexcodec - # pybtex - # python-dateutil - # sphinxcontrib-autoprogram -smmap==5.0.0 \ - --hash=sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94 \ - --hash=sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936 + # anyio + # asgi-lifespan + # httpx +testcontainers==4.8.1 \ + --hash=sha256:5ded4820b7227ad526857eb3caaafcabce1bbac05d22ad194849b136ffae3cb0 \ + --hash=sha256:d8ae43e8fe34060fcd5c3f494e0b7652b7774beabe94568a2283d0881e94d489 + # via -r requirements/dev.in +types-pyyaml==6.0.12.20240917 \ + --hash=sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570 \ + --hash=sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587 + # via -r requirements/dev.in +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via # -c requirements/main.txt - # gitdb -sniffio==1.2.0 \ - --hash=sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663 \ - --hash=sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de - # via anyio -snowballstemmer==2.2.0 \ - --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ - --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a - # via sphinx -sphinx==5.1.1 \ - --hash=sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693 \ - --hash=sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89 - # via - # documenteer - # numpydoc - # sphinx-automodapi - # sphinx-click - # sphinx-jinja - # sphinx-prompt - # sphinxcontrib-autoprogram - # sphinxcontrib-bibtex - # sphinxcontrib-doxylink -sphinx-automodapi==0.14.1 \ - --hash=sha256:4238e131d7abc47226449661bb3cfa2bb1b5b190184ffa69d9b924b984a22753 \ - --hash=sha256:a2f9c0f9e2901875e6db75df6c01412875eb15f25e7db1206e1b69fedf75bbc9 - # via documenteer -sphinx-click==4.3.0 \ - --hash=sha256:23e85a3cb0b728a421ea773699f6acadefae171d1a764a51dd8ec5981503ccbe \ - --hash=sha256:bd4db5d3c1bec345f07af07b8e28a76cfc5006d997984e38ae246bbf8b9a3b38 - # via documenteer -sphinx-jinja==2.0.2 \ - --hash=sha256:705ebeb9b7a6018ca3f93724315a7c1effa6ba3db44d630e7eaaa15e4ac081a8 \ - --hash=sha256:c6232b59a894139770be1dc6d0b00a379e4288ce78157904e1f8473dea3e0718 - # via documenteer -sphinx-prompt==1.5.0 \ - --hash=sha256:fa4e90d8088b5a996c76087d701fc7e31175f8b9dc4aab03a507e45051067162 - # via documenteer -sphinxcontrib-applehelp==1.0.2 \ - --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ - --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 - # via sphinx -sphinxcontrib-autoprogram==0.1.7 \ - --hash=sha256:746adb4214c3d2917af948499b3ed4b7b88b208a48c96368c0cff356474dba42 \ - --hash=sha256:bc642e3f2817a7539f306e021697f72b225bea5ad23b30dc14a7b9d1408d1f1a - # via documenteer -sphinxcontrib-bibtex==2.4.2 \ - --hash=sha256:608512afde6b732148cdc9123550bd560bf48e071d1fb7bb1bab4f4437ff04f4 \ - --hash=sha256:65b023ee47f35f1f03ac4d71c824e67c624c7ecac1bb26e83623271a01f9da86 - # via documenteer -sphinxcontrib-devhelp==1.0.2 \ - --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ - --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 - # via sphinx -sphinxcontrib-doxylink==1.12.0 \ - --hash=sha256:03e871f5833c92f5c1b6f201ca5ab1d9299c12ec749333d3a1c667038baeb050 \ - --hash=sha256:654394567c0a2cc5e6d33dbac98ee10b2a90b22d2a9e19f974b38a287c0f84dd - # via documenteer -sphinxcontrib-htmlhelp==2.0.0 \ - --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ - --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 - # via sphinx -sphinxcontrib-jsmath==1.0.1 \ - --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ - --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 - # via sphinx -sphinxcontrib-qthelp==1.0.3 \ - --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ - --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 \ - --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ - --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 - # via sphinx -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via - # coverage # mypy - # pytest -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 - # via mypy -urllib3==1.26.11 \ - --hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \ - --hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a + # pydantic + # pydantic-core + # testcontainers +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 # via # -c requirements/main.txt + # docker # requests -watchgod==0.8.2 \ - --hash=sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce \ - --hash=sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450 - # via aiohttp-devtools -yarl==1.7.2 \ - --hash=sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac \ - --hash=sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8 \ - --hash=sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e \ - --hash=sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746 \ - --hash=sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98 \ - --hash=sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125 \ - --hash=sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d \ - --hash=sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d \ - --hash=sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986 \ - --hash=sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d \ - --hash=sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec \ - --hash=sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8 \ - --hash=sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee \ - --hash=sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3 \ - --hash=sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1 \ - --hash=sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd \ - --hash=sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b \ - --hash=sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de \ - --hash=sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0 \ - --hash=sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8 \ - --hash=sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6 \ - --hash=sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245 \ - --hash=sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23 \ - --hash=sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332 \ - --hash=sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1 \ - --hash=sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c \ - --hash=sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4 \ - --hash=sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0 \ - --hash=sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8 \ - --hash=sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832 \ - --hash=sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58 \ - --hash=sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6 \ - --hash=sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1 \ - --hash=sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52 \ - --hash=sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92 \ - --hash=sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185 \ - --hash=sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d \ - --hash=sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d \ - --hash=sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b \ - --hash=sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739 \ - --hash=sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05 \ - --hash=sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63 \ - --hash=sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d \ - --hash=sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa \ - --hash=sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913 \ - --hash=sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe \ - --hash=sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b \ - --hash=sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b \ - --hash=sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656 \ - --hash=sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1 \ - --hash=sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4 \ - --hash=sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e \ - --hash=sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63 \ - --hash=sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271 \ - --hash=sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed \ - --hash=sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d \ - --hash=sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda \ - --hash=sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265 \ - --hash=sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f \ - --hash=sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c \ - --hash=sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba \ - --hash=sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c \ - --hash=sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b \ - --hash=sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523 \ - --hash=sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a \ - --hash=sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef \ - --hash=sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95 \ - --hash=sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72 \ - --hash=sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794 \ - --hash=sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41 \ - --hash=sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576 \ - --hash=sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59 - # via - # -c requirements/main.txt - # aiohttp -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 - # via importlib-metadata + # testcontainers +wrapt==1.16.0 \ + --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \ + --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \ + --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \ + --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \ + --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \ + --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \ + --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \ + --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \ + --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \ + --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \ + --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \ + --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \ + --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \ + --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \ + --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \ + --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \ + --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \ + --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \ + --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \ + --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \ + --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \ + --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \ + --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \ + --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \ + --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \ + --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \ + --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \ + --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \ + --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \ + --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \ + --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \ + --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \ + --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \ + --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \ + --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \ + --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \ + --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \ + --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \ + --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \ + --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \ + --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \ + --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \ + --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \ + --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \ + --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \ + --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \ + --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \ + --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \ + --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \ + --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \ + --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \ + --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \ + --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \ + --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \ + --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \ + --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \ + --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \ + --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \ + --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \ + --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \ + --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \ + --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \ + --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \ + --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \ + --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \ + --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \ + --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \ + --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \ + --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \ + --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4 + # via testcontainers diff --git a/requirements/main.in b/requirements/main.in index daa3844..d135627 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -1,3 +1,5 @@ +# -*- conf -*- +# # Editable runtime dependencies (equivalent to install_requires) # Add direct runtime dependencies here, as well as implicit dependencies # with constrained versions. @@ -5,17 +7,20 @@ # After editing, update requirements/main.txt by running: # make update-deps -aiodns -aiohttp -cchardet -structlog -colorama -click -fastavro -kafkit -aiokafka +# These dependencies are for fastapi including some optional features. +fastapi<0.112.2 # https://github.com/airtai/faststream/issues/1742#issuecomment-2320630668 +starlette +uvicorn[standard] + +# Other dependencies. +pydantic +pydantic-settings +safir[redis]>=5 +faststream[kafka] +rubin-squarebot>=0.10.0 templatekit -confluent-kafka +cookiecutter<2.2 # match templates repo GitPython cachetools gidgethub +pylatexenc diff --git a/requirements/main.txt b/requirements/main.txt index 6d2e8e3..b90df13 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -1,419 +1,386 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile --generate-hashes --output-file=requirements/main.txt requirements/main.in -# -aiodns==3.0.0 \ - --hash=sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2 \ - --hash=sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6 - # via -r requirements/main.in -aiohttp==3.8.1 \ - --hash=sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3 \ - --hash=sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782 \ - --hash=sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75 \ - --hash=sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf \ - --hash=sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7 \ - --hash=sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675 \ - --hash=sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1 \ - --hash=sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785 \ - --hash=sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4 \ - --hash=sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf \ - --hash=sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5 \ - --hash=sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15 \ - --hash=sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca \ - --hash=sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8 \ - --hash=sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac \ - --hash=sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8 \ - --hash=sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef \ - --hash=sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516 \ - --hash=sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700 \ - --hash=sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2 \ - --hash=sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8 \ - --hash=sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0 \ - --hash=sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676 \ - --hash=sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad \ - --hash=sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155 \ - --hash=sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db \ - --hash=sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd \ - --hash=sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091 \ - --hash=sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602 \ - --hash=sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411 \ - --hash=sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93 \ - --hash=sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd \ - --hash=sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec \ - --hash=sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51 \ - --hash=sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7 \ - --hash=sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17 \ - --hash=sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d \ - --hash=sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00 \ - --hash=sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923 \ - --hash=sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440 \ - --hash=sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32 \ - --hash=sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e \ - --hash=sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1 \ - --hash=sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724 \ - --hash=sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a \ - --hash=sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8 \ - --hash=sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2 \ - --hash=sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33 \ - --hash=sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b \ - --hash=sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2 \ - --hash=sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632 \ - --hash=sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b \ - --hash=sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2 \ - --hash=sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316 \ - --hash=sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74 \ - --hash=sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96 \ - --hash=sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866 \ - --hash=sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44 \ - --hash=sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950 \ - --hash=sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa \ - --hash=sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c \ - --hash=sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a \ - --hash=sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd \ - --hash=sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd \ - --hash=sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9 \ - --hash=sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421 \ - --hash=sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2 \ - --hash=sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922 \ - --hash=sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4 \ - --hash=sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237 \ - --hash=sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642 \ - --hash=sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578 - # via -r requirements/main.in -aiokafka==0.7.2 \ - --hash=sha256:16731e8aa0fc70dc35c31041599c9a5237dd5d2c1a4d04af58f30a942191a281 \ - --hash=sha256:1a9b6ecb606062b3595bc5104b85b42b62621a86d179b75d708279041152f461 \ - --hash=sha256:34eda0b6eb794c36f4100be772f3b120a3c00daaf342f593f32994a762aed7e8 \ - --hash=sha256:383cc7d45b47676fea60dbedee747c5c08dde5c10b1be0afc6598fb21a7891b4 \ - --hash=sha256:3b1f1e9ad66883ed809d737d57edfb13f1aeb9b08c6fd6b71afefce712c13dad \ - --hash=sha256:5371bd663f545ced555775e7e49f39a54b243435098a9699582bb3b32884e332 \ - --hash=sha256:594d2a29875f78d56251141ff95a982c5be64844dc9ae619c285f36c57a08c6e \ - --hash=sha256:6116b68ca975caafd7efd338ffdaec63789e1c334af6174e20edc1d16d14e463 \ - --hash=sha256:7d56627c3250ba2359dfa90f2c8a3ab995795e0116038905b2f8a608cd1fd606 \ - --hash=sha256:7fe02a3868236d84356d5fa7c7625ed3a27e52699477c5ee8bd5dc9b5adb592f \ - --hash=sha256:a3bfe4ad7d3829a98c8391a9a28f179b47df4f66e26ea5b1c665f872b6e21809 \ - --hash=sha256:a8fc41d18731d8879483aecb93ae7ebf5457f63daf4c8923ddc046792c2c3096 \ - --hash=sha256:b1958277eaa702509591c0674790a8c9aced8fef7723dafae0f9aec6d2da71a5 \ - --hash=sha256:b19f077e12fe23e359f7a7dca9baf8532c63f4c8149703ce4c56de372d17e26c \ - --hash=sha256:be43d7ddd700d501a6f4c59c859baa9888c2d086b69882f542951bae41234f6a \ - --hash=sha256:c96824cef1a480fd2ab4bbd9e9aa737c9191211bab5f7787ef401926d5fda95d \ - --hash=sha256:cda55f5cfb19ea7d2f55a51d320a57312f152dab3e333fa1fbfcdde7a9e25a53 \ - --hash=sha256:ce23baeaacf501f619967067d2c0d4c2b2b761012f9f9c8a49593e96c7550aff \ - --hash=sha256:d801bb2f5a4ae726a433ae74a5d34e7e0d44128de53c9c7eea5cb4cdaaf80175 \ - --hash=sha256:e6e78206e5d031e6644d3a46153a146d2d2afff4cf9da9a81edb9f8714114b62 \ - --hash=sha256:ebbb258840f134bad0e6ca8681a87cd292a1f4ed7253a821c16b4e9f2610a04a - # via -r requirements/main.in -aiosignal==1.2.0 \ - --hash=sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a \ - --hash=sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2 - # via aiohttp -arrow==1.2.2 \ - --hash=sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b \ - --hash=sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177 +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --output-file requirements/main.txt requirements/main.in +aiokafka==0.11.0 \ + --hash=sha256:0973a245b8b9daf8ef6814253a80a700f1f54d2da7d88f6fe479f46e0fd83053 \ + --hash=sha256:0d80590c4ef0ba546a299cee22ea27c3360c14241ec43a8e6904653f7b22d328 \ + --hash=sha256:0e957b42ae959365efbb45c9b5de38032c573608553c3670ad8695cc210abec9 \ + --hash=sha256:1d519bf9875ac867fb19d55de3750833b1eb6379a08de29a68618e24e6a49fc0 \ + --hash=sha256:1f8ae91f0373830e4664376157fe61b611ca7e573d8a559b151aef5bf53df46c \ + --hash=sha256:224db2447f6c1024198d8342e7099198f90401e2fa29c0762afbc51eadf5c490 \ + --hash=sha256:230170ce2e8a0eb852e2e8b78b08ce2e29b77dfe2c51bd56f5ab4be0f332a63b \ + --hash=sha256:3711fa64ee8640dcd4cb640f1030f9439d02e85acd57010d09053017092d8cc2 \ + --hash=sha256:38e1917e706c1158d5e1f612d1fc1b40f706dc46c534e73ab4de8ae2868a31be \ + --hash=sha256:419dd28c8ed6e926061bdc60929af08a6b52f1721e1179d9d21cc72ae28fd6f6 \ + --hash=sha256:4e0cc080a7f4c659ee4e1baa1c32adedcccb105a52156d4909f357d76fac0dc1 \ + --hash=sha256:516e1d68d9a377860b2e17453580afe304605bc71894f684d3e7b6618f6f939f \ + --hash=sha256:55a07a39d82c595223a17015ea738d152544cee979d3d6d822707a082465621c \ + --hash=sha256:560839ae6bc13e71025d71e94df36980f5c6e36a64916439e598b6457267a37f \ + --hash=sha256:59f4b935589ebb244620afad8bf3320e3bc86879a8b1c692ad06bd324f6c6127 \ + --hash=sha256:6ef3e7c8a923e502caa4d24041f2be778fd7f9ee4587bf0bcb4f74cac05122fa \ + --hash=sha256:702aec15b63bad5e4476294bcb1cb177559149fce3e59335794f004c279cbd6a \ + --hash=sha256:73584be8ba7906e3f33ca0f08f6af21a9ae31b86c6b635b93db3b1e6f452657b \ + --hash=sha256:807f699cf916369b1a512e4f2eaec714398c202d8803328ef8711967d99a56ce \ + --hash=sha256:818a6f8e44b02113b9e795bee6029c8a4e525ab38f29d7adb0201f3fec74c808 \ + --hash=sha256:8ba981956243767b37c929845c398fda2a2e35a4034d218badbe2b62e6f98f96 \ + --hash=sha256:926f93fb6a39891fd4364494432b479c0602f9cac708778d4a262a2c2e20d3b4 \ + --hash=sha256:9a478a14fd23fd1ffe9c7a21238d818b5f5e0626f7f06146b687f3699298391b \ + --hash=sha256:acfd0a5d0aec762ba73eeab73b23edce14f315793f063b6a4b223b6f79e36bb8 \ + --hash=sha256:d59fc7aec088c9ffc02d37e61591f053459bd11912cf04c70ac4f7e60405667d \ + --hash=sha256:d724b6fc484e453b373052813e4e543fc028a22c3fbda10e13b6829740000b8a \ + --hash=sha256:eaafe134de57b184f3c030e1a11051590caff7953c8bf58048eefd8d828e39d7 \ + --hash=sha256:eac78a009b713e28b5b4c4daae9d062acbf2b7980e5734467643a810134583b5 \ + --hash=sha256:ee0c61a2dcabbe4474ff237d708f9bd663dd2317e03a9cb7239a212c9ee05b12 \ + --hash=sha256:f1c85f66eb3564c5e74d8e4c25df4ac1fd94f1a6f6e66f005aafa6f791bde215 \ + --hash=sha256:f2def07fe1720c4fe37c0309e355afa9ff4a28e0aabfe847be0692461ac69352 + # via faststream +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.6.0 \ + --hash=sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb \ + --hash=sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a + # via + # fast-depends + # faststream + # httpx + # starlette + # watchfiles +arrow==1.3.0 \ + --hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 \ + --hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85 # via jinja2-time -async-timeout==4.0.2 \ - --hash=sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15 \ - --hash=sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c - # via aiohttp -attrs==21.4.0 \ - --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ - --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd - # via aiohttp +async-timeout==4.0.3 \ + --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \ + --hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028 + # via aiokafka binaryornot==0.4.4 \ --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 # via cookiecutter -cachetools==5.2.0 \ - --hash=sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757 \ - --hash=sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db - # via -r requirements/main.in -cchardet==2.1.7 \ - --hash=sha256:0b859069bbb9d27c78a2c9eb997e6f4b738db2d7039a03f8792b4058d61d1109 \ - --hash=sha256:228d2533987c450f39acf7548f474dd6814c446e9d6bd228e8f1d9a2d210f10b \ - --hash=sha256:2309ff8fc652b0fc3c0cff5dbb172530c7abb92fe9ba2417c9c0bcf688463c1c \ - --hash=sha256:24974b3e40fee9e7557bb352be625c39ec6f50bc2053f44a3d1191db70b51675 \ - --hash=sha256:273699c4e5cd75377776501b72a7b291a988c6eec259c29505094553ee505597 \ - --hash=sha256:27a9ba87c9f99e0618e1d3081189b1217a7d110e5c5597b0b7b7c3fedd1c340a \ - --hash=sha256:302aa443ae2526755d412c9631136bdcd1374acd08e34f527447f06f3c2ddb98 \ - --hash=sha256:45456c59ec349b29628a3c6bfb86d818ec3a6fbb7eb72de4ff3bd4713681c0e3 \ - --hash=sha256:48ba829badef61441e08805cfa474ccd2774be2ff44b34898f5854168c596d4d \ - --hash=sha256:50ad671e8d6c886496db62c3bd68b8d55060688c655873aa4ce25ca6105409a1 \ - --hash=sha256:54341e7e1ba9dc0add4c9d23b48d3a94e2733065c13920e85895f944596f6150 \ - --hash=sha256:54d0b26fd0cd4099f08fb9c167600f3e83619abefeaa68ad823cc8ac1f7bcc0c \ - --hash=sha256:5a25f9577e9bebe1a085eec2d6fdd72b7a9dd680811bba652ea6090fb2ff472f \ - --hash=sha256:6b6397d8a32b976a333bdae060febd39ad5479817fabf489e5596a588ad05133 \ - --hash=sha256:70eeae8aaf61192e9b247cf28969faef00578becd2602526ecd8ae7600d25e0e \ - --hash=sha256:80e6faae75ecb9be04a7b258dc4750d459529debb6b8dee024745b7b5a949a34 \ - --hash=sha256:90086e5645f8a1801350f4cc6cb5d5bf12d3fa943811bb08667744ec1ecc9ccd \ - --hash=sha256:a39526c1c526843965cec589a6f6b7c2ab07e3e56dc09a7f77a2be6a6afa4636 \ - --hash=sha256:b154effa12886e9c18555dfc41a110f601f08d69a71809c8d908be4b1ab7314f \ - --hash=sha256:b59ddc615883835e03c26f81d5fc3671fab2d32035c87f50862de0da7d7db535 \ - --hash=sha256:bd7f262f41fd9caf5a5f09207a55861a67af6ad5c66612043ed0f81c58cdf376 \ - --hash=sha256:c428b6336545053c2589f6caf24ea32276c6664cb86db817e03a94c60afa0eaf \ - --hash=sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f \ - --hash=sha256:c96aee9ebd1147400e608a3eff97c44f49811f8904e5a43069d55603ac4d8c97 \ - --hash=sha256:ec3eb5a9c475208cf52423524dcaf713c394393e18902e861f983c38eeb77f18 \ - --hash=sha256:eee4f5403dc3a37a1ca9ab87db32b48dc7e190ef84601068f45397144427cc5e \ - --hash=sha256:f16517f3697569822c6d09671217fdeab61dfebc7acb5068634d6b0728b86c0b \ - --hash=sha256:f86e0566cb61dc4397297696a4a1b30f6391b50bc52b4f073507a48466b6255a \ - --hash=sha256:fdac1e4366d0579fff056d1280b8dc6348be964fda8ebb627c0269e097ab37fa +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a # via -r requirements/main.in -cerberus==1.3.4 \ - --hash=sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c +cerberus==1.3.5 \ + --hash=sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372 \ + --hash=sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642 # via templatekit -certifi==2022.6.15 \ - --hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \ - --hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412 - # via requests -cffi==1.15.1 \ - --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ - --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ - --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ - --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ - --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ - --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ - --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ - --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ - --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ - --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ - --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ - --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ - --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ - --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ - --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ - --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ - --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ - --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ - --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ - --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ - --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ - --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ - --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ - --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ - --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ - --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ - --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ - --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ - --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ - --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ - --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ - --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ - --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ - --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ - --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ - --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ - --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ - --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ - --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ - --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ - --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ - --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ - --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ - --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ - --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ - --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ - --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ - --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ - --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ - --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ - --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ - --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ - --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ - --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ - --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ - --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ - --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ - --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ - --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ - --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ - --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ - --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ - --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ - --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 - # via - # cryptography - # pycares -chardet==5.0.0 \ - --hash=sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa \ - --hash=sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557 - # via binaryornot -charset-normalizer==2.1.0 \ - --hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \ - --hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413 +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 # via - # aiohttp + # httpcore + # httpx # requests -click==8.1.3 \ - --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ - --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 +cffi==1.17.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ + --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ + --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ + --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ + --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ + --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ + --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ + --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ + --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ + --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ + --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ + --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ + --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ + --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ + --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ + --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ + --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ + --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ + --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ + --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ + --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ + --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ + --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ + --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ + --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ + --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ + --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ + --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ + --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ + --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ + --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ + --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ + --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ + --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ + --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ + --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ + --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ + --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ + --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ + --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ + --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ + --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ + --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ + --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ + --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ + --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ + --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ + --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ + --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ + --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ + --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ + --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ + --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ + --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ + --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ + --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ + --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ + --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ + --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ + --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ + --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ + --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ + --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ + --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ + --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ + --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ + --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b + # via cryptography +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via binaryornot +charset-normalizer==3.3.2 \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 + # via requests +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via - # -r requirements/main.in # cookiecutter + # safir # templatekit -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 - # via -r requirements/main.in -confluent-kafka==1.9.0 \ - --hash=sha256:066856f43b248c6634f43a01bb5798c1acf8553fcad0ee04f353643a2473b788 \ - --hash=sha256:0eca04a9321de8172a5563d79ee3d09b93ec5811616502361af96b22070465ef \ - --hash=sha256:1446e5d8502694f4a9140ace087567b85300bd9766a532e006f2f6e9c6e99284 \ - --hash=sha256:2008aeca66112722d3bcbff0baa40e129bdc6747755d5e71409493f2b56ffa36 \ - --hash=sha256:2f87004473718d1976f57c9e23307c216daf8cbd47ffa7115ef0ce48963c9c69 \ - --hash=sha256:41590186d9528b4a9e3a6116551918471f41e82f54438742f1c8ecbe2441d1c3 \ - --hash=sha256:45fa3b9d4d015c099dcb5fd77cbc0d40f6683afe9a9b279c82fb0ed814e11724 \ - --hash=sha256:49312cd1dce9963578669412831c55a83dc42ced0af975fe8a885860d4439505 \ - --hash=sha256:4a93eccaab455ab4ad70cdd22f2e15b9fd5607a019dd08cd95b04308193f84cb \ - --hash=sha256:5559cb175cb668f53e4a053d921a06393aaf2a16b14cd807439e93d7100b1594 \ - --hash=sha256:7cec61388faf7d122ae0bfb7948bdb1333ad8bd95a6cdc50eac97342e0faff75 \ - --hash=sha256:80b87db486566067f7a24955f33db0fe74af977938c917216fe709b0332f668e \ - --hash=sha256:9ea5916b7537e5679011387336c101433567587d3862351e75c8bb8d0dc3d838 \ - --hash=sha256:c3bfe2a13efb0bb810146466175038c0202b8e5f14a9a37ca494bbd777f26870 \ - --hash=sha256:dee2f5d3a193dd0b5500e4e129b7da9f662b49f5a9b8a13cb6b8af72c69c534b \ - --hash=sha256:e1e0aeaff2afba8138d5c103589bde1c3d8f870606d00115df03adffbffd368f \ - --hash=sha256:f46a445556831a23b356bcc6211aab8a09b5e709ec956bce01237b3152eeb166 \ - --hash=sha256:f85c80a6b37bf60caa8118d2be205380e037e389769ee92b15cc733e3044e17a \ - --hash=sha256:fa1cbc113809a0aa1f8b33c4ec2cf246fcbc329c3b9e40054514d4c95ffd44e4 \ - --hash=sha256:fb23fd087695cf1753a43bf77173e9c87f535b9293b94b7f29f17461c1ba9eb5 \ - --hash=sha256:fdca4a62ed61ae8d77758f3df163c7959a1757a2cd37f0f54a15f7f347841b59 - # via -r requirements/main.in + # uvicorn +colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # click + # uvicorn cookiecutter==2.1.1 \ --hash=sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022 \ --hash=sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5 - # via templatekit -cryptography==37.0.4 \ - --hash=sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59 \ - --hash=sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596 \ - --hash=sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3 \ - --hash=sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5 \ - --hash=sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab \ - --hash=sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884 \ - --hash=sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82 \ - --hash=sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b \ - --hash=sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441 \ - --hash=sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa \ - --hash=sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d \ - --hash=sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b \ - --hash=sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a \ - --hash=sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6 \ - --hash=sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157 \ - --hash=sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280 \ - --hash=sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282 \ - --hash=sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67 \ - --hash=sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8 \ - --hash=sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046 \ - --hash=sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327 \ - --hash=sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9 - # via pyjwt -fastavro==1.5.3 \ - --hash=sha256:063f17e000b7a4174ad362ffc91b75de85970cc67fccfed2bd12e21908de6028 \ - --hash=sha256:14c23dc870785d860f1523e7be7ce4079dc36070f1bad18139304a5f4d654180 \ - --hash=sha256:259ade020a70870858b656219d25f566eb620e52967070f9e18b040f1c0cd482 \ - --hash=sha256:262a9f629448a0d4941a4871fd958b70a01f48a0106644b40e4fbaf984f5e89e \ - --hash=sha256:298e98c55e98f579df66e8258a942494a64b5340212968ed1b6bd9a68c52db5f \ - --hash=sha256:2c14c72f79bcb689edea06ec276bfd5c4d4f17b8e32f545e32a6ee108c5f09de \ - --hash=sha256:3a4fa9f72c9b134fcdb0ee30d4a838f1d30656ece72a95f18cc74db10148ee55 \ - --hash=sha256:6ad6af14d5e927e0c5fbc5dcb7c7030ef8759c8011620344a57252c263d6dd5a \ - --hash=sha256:78ffb34790791a82edef9cce7e49e44faf1b6b0b1da12e9c7d45f31fd6655a7b \ - --hash=sha256:86a33b2f819cad67f002250e9c6a62909fc31e048679da5f2ac02f4bbf0a5998 \ - --hash=sha256:8ce66549bdbc9d43f40e1858796c1db59fa9f63f2368217071929c39f7ee7c1e \ - --hash=sha256:9b399317d4f559d8f3ef695ef8db8ea230e4b489437d2a1e50307120e181e831 \ - --hash=sha256:b0408d6b02abcbd4e8866b6b027d7cdd20d570e78177fb7c363fb3cc8ecb02a3 \ - --hash=sha256:badcbd1c34f446f4b64ade84f9517ef7d52197e0ea9dd6b3775274f2271dba9a \ - --hash=sha256:bcaf1c9ead0aa1c7306cff229f2c207d714f7f765e0bb010e64c74d379008555 \ - --hash=sha256:c5043cfbdfed27f1331860f1d215db838b92d575e8a78915c52c3f6c64d42176 \ - --hash=sha256:f8980f2bc1230aac509273938d23c544283a476bf84ac79b80540d503f941fe5 # via # -r requirements/main.in - # kafkit -frozenlist==1.3.0 \ - --hash=sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e \ - --hash=sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08 \ - --hash=sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b \ - --hash=sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486 \ - --hash=sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78 \ - --hash=sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468 \ - --hash=sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1 \ - --hash=sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953 \ - --hash=sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3 \ - --hash=sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d \ - --hash=sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a \ - --hash=sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141 \ - --hash=sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08 \ - --hash=sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07 \ - --hash=sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa \ - --hash=sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa \ - --hash=sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868 \ - --hash=sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f \ - --hash=sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b \ - --hash=sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b \ - --hash=sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1 \ - --hash=sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f \ - --hash=sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478 \ - --hash=sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58 \ - --hash=sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01 \ - --hash=sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8 \ - --hash=sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d \ - --hash=sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676 \ - --hash=sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274 \ - --hash=sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab \ - --hash=sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8 \ - --hash=sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24 \ - --hash=sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a \ - --hash=sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2 \ - --hash=sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f \ - --hash=sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f \ - --hash=sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93 \ - --hash=sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1 \ - --hash=sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51 \ - --hash=sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846 \ - --hash=sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5 \ - --hash=sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d \ - --hash=sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c \ - --hash=sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e \ - --hash=sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae \ - --hash=sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02 \ - --hash=sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0 \ - --hash=sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b \ - --hash=sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3 \ - --hash=sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b \ - --hash=sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa \ - --hash=sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a \ - --hash=sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d \ - --hash=sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed \ - --hash=sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148 \ - --hash=sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9 \ - --hash=sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c \ - --hash=sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2 \ - --hash=sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951 + # templatekit +cryptography==43.0.1 \ + --hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \ + --hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \ + --hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \ + --hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \ + --hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \ + --hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \ + --hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \ + --hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \ + --hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \ + --hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \ + --hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \ + --hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \ + --hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \ + --hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \ + --hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \ + --hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \ + --hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \ + --hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \ + --hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \ + --hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \ + --hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \ + --hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \ + --hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \ + --hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \ + --hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \ + --hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \ + --hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289 + # via + # pyjwt + # safir +fast-depends==2.4.11 \ + --hash=sha256:29c8022232d1f67408d41fc251d2756c74ba98298e0502454de9f42df99a2ab0 \ + --hash=sha256:f361d6b6375184a800e93ecaf874a5ebc9905fe4f5fafecc50de2c81acf850d8 + # via faststream +fastapi==0.112.1 \ + --hash=sha256:b2537146f8c23389a7faa8b03d0bd38d4986e6983874557d95eed2acc46448ef \ + --hash=sha256:bcbd45817fc2a1cd5da09af66815b84ec0d3d634eb173d1ab468ae3103e183e4 # via - # aiohttp - # aiosignal -gidgethub==5.2.0 \ - --hash=sha256:390caafb3e9c6125bb9c8c9865a78d09e028f19592352d5b88e0ed881b8dea18 \ - --hash=sha256:c359b769194e72f984d2e328de0ee8eb81b702342b0a44dc5cebac92104ecf4b + # -r requirements/main.in + # safir +faststream==0.5.25 \ + --hash=sha256:872d88a112d3d9c454dbc0fa4fe4211717e4422b3aa2f75f9d239bffc3c31075 \ + --hash=sha256:926a4a8e013140a37b1c06ad16eb9a35529c9f626def3d846ec4ac171e050bc6 # via -r requirements/main.in -gitdb==4.0.9 \ - --hash=sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd \ - --hash=sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa +gidgethub==5.3.0 \ + --hash=sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0 \ + --hash=sha256:9ece7d37fbceb819b80560e7ed58f936e48a65d37ec5f56db79145156b426a25 + # via + # -r requirements/main.in + # safir +gitdb==4.0.11 \ + --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ + --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b # via gitpython -gitpython==3.1.27 \ - --hash=sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704 \ - --hash=sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d +gitpython==3.1.43 \ + --hash=sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c \ + --hash=sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff # via # -r requirements/main.in # templatekit -idna==3.3 \ - --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ - --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 # via + # httpcore + # uvicorn +httpcore==1.0.6 \ + --hash=sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f \ + --hash=sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f + # via httpx +httptools==0.6.1 \ + --hash=sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563 \ + --hash=sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142 \ + --hash=sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d \ + --hash=sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b \ + --hash=sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4 \ + --hash=sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb \ + --hash=sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658 \ + --hash=sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084 \ + --hash=sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2 \ + --hash=sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97 \ + --hash=sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837 \ + --hash=sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3 \ + --hash=sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58 \ + --hash=sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da \ + --hash=sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d \ + --hash=sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90 \ + --hash=sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0 \ + --hash=sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1 \ + --hash=sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2 \ + --hash=sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e \ + --hash=sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0 \ + --hash=sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf \ + --hash=sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc \ + --hash=sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3 \ + --hash=sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503 \ + --hash=sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a \ + --hash=sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3 \ + --hash=sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949 \ + --hash=sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84 \ + --hash=sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb \ + --hash=sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a \ + --hash=sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f \ + --hash=sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e \ + --hash=sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81 \ + --hash=sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185 \ + --hash=sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3 + # via uvicorn +httpx==0.27.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 + # via safir +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # anyio + # httpx # requests - # yarl -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # via # cookiecutter # jinja2-time @@ -422,321 +389,555 @@ jinja2-time==0.2.0 \ --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa # via cookiecutter -kafka-python==2.0.2 \ - --hash=sha256:04dfe7fea2b63726cd6f3e79a2d86e709d608d74406638c5da33a01d45a9d7e3 \ - --hash=sha256:2d92418c7cb1c298fa6c7f0fb3519b520d0d7526ac6cb7ae2a4fc65a51a94b6e - # via aiokafka -kafkit==0.2.1 \ - --hash=sha256:4cb9917ce25699a910be2b02812d9d024d9eaef6ac1c42181246119733a5069a \ - --hash=sha256:e372f66e710a344dfc0c370a05974e354dba56ea298c495517e68768b61cfaf5 - # via -r requirements/main.in -markupsafe==2.1.1 \ - --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \ - --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \ - --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \ - --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \ - --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \ - --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \ - --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \ - --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \ - --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \ - --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \ - --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \ - --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \ - --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \ - --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \ - --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \ - --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \ - --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \ - --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \ - --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \ - --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \ - --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \ - --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \ - --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \ - --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \ - --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \ - --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \ - --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \ - --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \ - --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \ - --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \ - --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \ - --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \ - --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \ - --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \ - --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \ - --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \ - --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \ - --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \ - --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \ - --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7 +markupsafe==3.0.0 \ + --hash=sha256:03ff62dea2fef3eadf2f1853bc6332bcb0458d9608b11dfb1cd5aeda1c178ea6 \ + --hash=sha256:105ada43a61af22acb8774514c51900dc820c481cc5ba53f17c09d294d9c07ca \ + --hash=sha256:12ddac720b8965332d36196f6f83477c6351ba6a25d4aff91e30708c729350d7 \ + --hash=sha256:1d151b9cf3307e259b749125a5a08c030ba15a8f1d567ca5bfb0e92f35e761f5 \ + --hash=sha256:1ee9790be6f62121c4c58bbced387b0965ab7bffeecb4e17cc42ef290784e363 \ + --hash=sha256:1fd02f47596e00a372f5b4af2b4c45f528bade65c66dfcbc6e1ea1bfda758e98 \ + --hash=sha256:23efb2be7221105c8eb0e905433414d2439cb0a8c5d5ca081c1c72acef0f5613 \ + --hash=sha256:25396abd52b16900932e05b7104bcdc640a4d96c914f39c3b984e5a17b01fba0 \ + --hash=sha256:27d6a73682b99568916c54a4bfced40e7d871ba685b580ea04bbd2e405dfd4c5 \ + --hash=sha256:380faf314c3c84c1682ca672e6280c6c59e92d0bc13dc71758ffa2de3cd4e252 \ + --hash=sha256:3b231255770723f1e125d63c14269bcd8b8136ecfb620b9a18c0297e046d0736 \ + --hash=sha256:3cd0bba31d484fe9b9d77698ddb67c978704603dc10cdc905512af308cfcca6b \ + --hash=sha256:3efde9a8c56c3b6e5f3fa4baea828f8184970c7c78480fedb620d804b1c31e5c \ + --hash=sha256:409535e0521c4630d5b5a1bf284e9d3c76d2fc2f153ebb12cf3827797798cc99 \ + --hash=sha256:494a64efc535e147fcc713dba58eecfce3a79f1e93ebe81995b387f5cd9bc2e1 \ + --hash=sha256:4ca04c60006867610a06575b46941ae616b19da0adc85b9f8f3d9cbd7a3da385 \ + --hash=sha256:4deea1d9169578917d1f35cdb581bc7bab56a7e8c5be2633bd1b9549c3c22a01 \ + --hash=sha256:509c424069dd037d078925b6815fc56b7271f3aaec471e55e6fa513b0a80d2aa \ + --hash=sha256:5509a8373fed30b978557890a226c3d30569746c565b9daba69df80c160365a5 \ + --hash=sha256:59420b5a9a5d3fee483a32adb56d7369ae0d630798da056001be1e9f674f3aa6 \ + --hash=sha256:5d207ff5cceef77796f8aacd44263266248cf1fbc601441524d7835613f8abec \ + --hash=sha256:5ddf5cb8e9c00d9bf8b0c75949fb3ff9ea2096ba531693e2e87336d197fdb908 \ + --hash=sha256:63dae84964a9a3d2610808cee038f435d9a111620c37ccf872c2fcaeca6865b3 \ + --hash=sha256:64a7c7856c3a409011139b17d137c2924df4318dab91ee0530800819617c4381 \ + --hash=sha256:64f7d04410be600aa5ec0626d73d43e68a51c86500ce12917e10fd013e258df5 \ + --hash=sha256:658fdf6022740896c403d45148bf0c36978c6b48c9ef8b1f8d0c7a11b6cdea86 \ + --hash=sha256:678fbceb202382aae42c1f0cd9f56b776bc20a58ae5b553ee1fe6b802983a1d6 \ + --hash=sha256:7835de4c56066e096407a1852e5561f6033786dd987fa90dc384e45b9bd21295 \ + --hash=sha256:7c524203207f5b569df06c96dafdc337228921ee8c3cc5f6e891d024c6595352 \ + --hash=sha256:7ed789d0f7f11fcf118cf0acb378743dfdd4215d7f7d18837c88171405c9a452 \ + --hash=sha256:81be2c0084d8c69e97e3c5d73ce9e2a6e523556f2a19c4e195c09d499be2f808 \ + --hash=sha256:81ee9c967956b9ea39b3a5270b7cb1740928d205b0dc72629164ce621b4debf9 \ + --hash=sha256:8219e2207f6c188d15614ea043636c2b36d2d79bf853639c124a179412325a13 \ + --hash=sha256:96e3ed550600185d34429477f1176cedea8293fa40e47fe37a05751bcb64c997 \ + --hash=sha256:98fb3a2bf525ad66db96745707b93ba0f78928b7a1cb2f1cb4b143bc7e2ba3b3 \ + --hash=sha256:9b36473a2d3e882d1873ea906ce54408b9588dc2c65989664e6e7f5a2de353d7 \ + --hash=sha256:9f91c90f8f3bf436f81c12eeb4d79f9ddd263c71125e6ad71341906832a34386 \ + --hash=sha256:a5fd5500d4e4f7cc88d8c0f2e45126c4307ed31e08f8ec521474f2fd99d35ac3 \ + --hash=sha256:a7171d2b869e9be238ea318c196baf58fbf272704e9c1cd4be8c380eea963342 \ + --hash=sha256:a80c6740e1bfbe50cea7cbf74f48823bb57bd59d914ee22ff8a81963b08e62d2 \ + --hash=sha256:b2a7afd24d408b907672015555bc10be2382e6c5f62a488e2d452da670bbd389 \ + --hash=sha256:b43ac1eb9f91e0c14aac1d2ef0f76bc7b9ceea51de47536f61268191adf52ad7 \ + --hash=sha256:b6cc46a27d904c9be5732029769acf4b0af69345172ed1ef6d4db0c023ff603b \ + --hash=sha256:b94bec9eda10111ec7102ef909eca4f3c2df979643924bfe58375f560713a7d1 \ + --hash=sha256:bd9b8e458e2bab52f9ad3ab5dc8b689a3c84b12b2a2f64cd9a0dfe209fb6b42f \ + --hash=sha256:c182d45600556917f811aa019d834a89fe4b6f6255da2fd0bdcf80e970f95918 \ + --hash=sha256:c409691696bec2b5e5c9efd9593c99025bf2f317380bf0d993ee0213516d908a \ + --hash=sha256:c5243044a927e8a6bb28517838662a019cd7f73d7f106bbb37ab5e7fa8451a92 \ + --hash=sha256:c8ab7efeff1884c5da8e18f743b667215300e09043820d11723718de0b7db934 \ + --hash=sha256:cb244adf2499aa37d5dc43431990c7f0b632d841af66a51d22bd89c437b60264 \ + --hash=sha256:d261ec38b8a99a39b62e0119ed47fe3b62f7691c500bc1e815265adc016438c1 \ + --hash=sha256:d2c099be5274847d606574234e494f23a359e829ba337ea9037c3a72b0851942 \ + --hash=sha256:d7e63d1977d3806ce0a1a3e0099b089f61abdede5238ca6a3f3bf8877b46d095 \ + --hash=sha256:dba0f83119b9514bc37272ad012f0cc03f0805cc6a2bea7244e19250ac8ff29f \ + --hash=sha256:dcbee57fedc9b2182c54ffc1c5eed316c3da8bbfeda8009e1b5d7220199d15da \ + --hash=sha256:e042ccf8fe5bf8b6a4b38b3f7d618eb10ea20402b0c9f4add9293408de447974 \ + --hash=sha256:e363440c8534bf2f2ef1b8fdc02037eb5fff8fce2a558519b22d6a3a38b3ec5e \ + --hash=sha256:e64b390a306f9e849ee809f92af6a52cda41741c914358e0e9f8499d03741526 \ + --hash=sha256:f0411641d31aa6f7f0cc13f0f18b63b8dc08da5f3a7505972a42ab059f479ba3 \ + --hash=sha256:f1c13c6c908811f867a8e9e66efb2d6c03d1cdd83e92788fe97f693c457dc44f \ + --hash=sha256:f846fd7c241e5bd4161e2a483663eb66e4d8e12130fcdc052f310f388f1d61c6 # via jinja2 -multidict==6.0.2 \ - --hash=sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60 \ - --hash=sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c \ - --hash=sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672 \ - --hash=sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51 \ - --hash=sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032 \ - --hash=sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2 \ - --hash=sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b \ - --hash=sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80 \ - --hash=sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88 \ - --hash=sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a \ - --hash=sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d \ - --hash=sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389 \ - --hash=sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c \ - --hash=sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9 \ - --hash=sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c \ - --hash=sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516 \ - --hash=sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b \ - --hash=sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43 \ - --hash=sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee \ - --hash=sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227 \ - --hash=sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d \ - --hash=sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae \ - --hash=sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7 \ - --hash=sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4 \ - --hash=sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9 \ - --hash=sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f \ - --hash=sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013 \ - --hash=sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9 \ - --hash=sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e \ - --hash=sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693 \ - --hash=sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a \ - --hash=sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15 \ - --hash=sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb \ - --hash=sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96 \ - --hash=sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87 \ - --hash=sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376 \ - --hash=sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658 \ - --hash=sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0 \ - --hash=sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071 \ - --hash=sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360 \ - --hash=sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc \ - --hash=sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3 \ - --hash=sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba \ - --hash=sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8 \ - --hash=sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9 \ - --hash=sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2 \ - --hash=sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3 \ - --hash=sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68 \ - --hash=sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8 \ - --hash=sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d \ - --hash=sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49 \ - --hash=sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608 \ - --hash=sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57 \ - --hash=sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86 \ - --hash=sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20 \ - --hash=sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293 \ - --hash=sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849 \ - --hash=sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937 \ - --hash=sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d - # via - # aiohttp - # yarl -pycares==4.2.1 \ - --hash=sha256:061dd4c80fec73feb150455b159704cd51a122f20d36790033bd6375d4198579 \ - --hash=sha256:15dd5cf21bc73ad539e8aabf7afe370d1df8af7bc6944cd7298f3bfef0c1a27c \ - --hash=sha256:1a9506d496efeb809a1b63647cb2f3f33c67fcf62bf80a2359af692fef2c1755 \ - --hash=sha256:1f37f762414680063b4dfec5be809a84f74cd8e203d939aaf3ba9c807a9e7013 \ - --hash=sha256:2113529004df4894783eaa61e9abc3a680756b6f033d942f2800301ae8c71c29 \ - --hash=sha256:2fd53eb5b441c4f6f9c78d7900e05883e9998b34a14b804be4fc4c6f9fea89f3 \ - --hash=sha256:3636fccf643c5192c34ee0183c514a2d09419e3a76ca2717cef626638027cb21 \ - --hash=sha256:396ee487178e9de06ca4122a35a157474db3ce0a0db6038a31c831ebb9863315 \ - --hash=sha256:3b78bdee2f2f1351d5fccc2d1b667aea2d15a55d74d52cb9fd5bea8b5e74c4dc \ - --hash=sha256:4ee625d7571039038bca51ae049b047cbfcfc024b302aae6cc53d5d9aa8648a8 \ - --hash=sha256:5333b51ef4ff3e8973b4a1b57cad5ada13e15552445ee3cd74bd77407dec9d44 \ - --hash=sha256:66b5390a4885a578e687d3f2683689c35e1d4573f4d0ecf217431f7bb55c49a0 \ - --hash=sha256:6724573e830ea2345f4bcf0f968af64cc6d491dc2133e9c617f603445dcdfa58 \ - --hash=sha256:735b4f75fd0f595c4e9184da18cd87737f46bc81a64ea41f4edce2b6b68d46d2 \ - --hash=sha256:7a901776163a04de5d67c42bd63a287cff9cb05fc041668ad1681fe3daa36445 \ - --hash=sha256:8bd6ed3ad3a5358a635c1acf5d0f46be9afb095772b84427ff22283d2f31db1b \ - --hash=sha256:99e00e397d07a79c9f43e4303e67f4f97bcabd013bda0d8f2d430509b7aef8a0 \ - --hash=sha256:9b05c2cec644a6c66b55bcf6c24d4dfdaf2f7205b16e5c4ceee31db104fac958 \ - --hash=sha256:a521d7f54f3e52ded4d34c306ba05cfe9eb5aaa2e5aaf83c96564b9369495588 \ - --hash=sha256:b03f69df69f0ab3bfb8dbe54444afddff6ff9389561a08aade96b4f91207a655 \ - --hash=sha256:c8a46839da642b281ac5f56d3c6336528e128b3c41eab9c5330d250f22325e9d \ - --hash=sha256:d2e8ec4c8e07c986b70a3cc8f5b297c53b08ac755e5b9797512002a466e2de86 \ - --hash=sha256:d83f193563b42360528167705b1c7bb91e2a09f990b98e3d6378835b72cd5c96 \ - --hash=sha256:d9cd826d8e0c270059450709bff994bfeb072f79d82fd3f11c701690ff65d0e7 \ - --hash=sha256:e4dc37f732f7110ca6368e0128cbbd0a54f5211515a061b2add64da2ddb8e5ca \ - --hash=sha256:e75cbd4d3b3d9b02bba6e170846e39893a825e7a5fb1b96728fc6d7b964f8945 \ - --hash=sha256:e7a95763cdc20cf9ec357066e656ea30b8de6b03de6175cbb50890e22aa01868 \ - --hash=sha256:e9dbfcacbde6c21380c412c13d53ea44b257dea3f7b9d80be2c873bb20e21fee \ - --hash=sha256:f05223de13467bb26f9a1594a1799ce2d08ad8ea241489fecd9d8ed3bbbfc672 \ - --hash=sha256:f8e6942965465ca98e212376c4afb9aec501d8129054929744b2f4a487c8c14b \ - --hash=sha256:fbd53728d798d07811898e11991e22209229c090eab265a53d12270b95d70d1a - # via aiodns -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via aiokafka +pycparser==2.22 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi -pyjwt[crypto]==2.4.0 \ - --hash=sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf \ - --hash=sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba +pydantic==2.9.2 \ + --hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \ + --hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12 + # via + # -r requirements/main.in + # fast-depends + # fastapi + # pydantic-settings + # rubin-squarebot + # safir +pydantic-core==2.23.4 \ + --hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \ + --hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \ + --hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \ + --hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \ + --hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \ + --hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \ + --hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \ + --hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \ + --hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \ + --hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \ + --hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \ + --hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \ + --hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \ + --hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \ + --hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \ + --hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \ + --hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \ + --hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \ + --hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \ + --hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \ + --hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \ + --hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \ + --hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \ + --hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \ + --hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \ + --hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \ + --hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \ + --hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \ + --hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \ + --hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \ + --hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \ + --hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \ + --hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \ + --hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \ + --hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \ + --hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \ + --hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \ + --hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \ + --hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \ + --hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \ + --hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \ + --hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \ + --hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \ + --hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \ + --hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \ + --hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \ + --hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \ + --hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \ + --hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \ + --hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \ + --hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \ + --hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \ + --hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \ + --hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \ + --hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \ + --hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \ + --hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \ + --hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \ + --hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \ + --hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \ + --hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \ + --hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \ + --hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \ + --hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \ + --hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \ + --hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \ + --hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \ + --hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \ + --hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \ + --hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \ + --hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \ + --hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \ + --hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \ + --hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \ + --hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \ + --hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \ + --hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \ + --hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \ + --hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \ + --hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \ + --hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \ + --hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \ + --hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \ + --hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \ + --hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \ + --hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \ + --hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \ + --hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \ + --hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607 + # via + # pydantic + # safir +pydantic-settings==2.5.2 \ + --hash=sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907 \ + --hash=sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0 + # via -r requirements/main.in +pyjwt==2.9.0 \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c # via gidgethub -pyperclip==1.8.2 \ - --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 +pylatexenc==2.10 \ + --hash=sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3 + # via -r requirements/main.in +pyperclip==1.9.0 \ + --hash=sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310 # via templatekit -python-dateutil==2.8.2 \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via arrow -python-slugify==6.1.2 \ - --hash=sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1 \ - --hash=sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927 +python-dotenv==1.0.1 \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a + # via + # pydantic-settings + # uvicorn +python-slugify==8.0.4 \ + --hash=sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8 \ + --hash=sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856 # via cookiecutter -pyyaml==6.0 \ - --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ - --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ - --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ - --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ - --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ - --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ - --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ - --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ - --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ - --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ - --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ - --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ - --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ - --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ - --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ - --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ - --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ - --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ - --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ - --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ - --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ - --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ - --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ - --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ - --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ - --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ - --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ - --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ - --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ - --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ - --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ - --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ - --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 # via # cookiecutter # templatekit -requests==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 + # uvicorn +redis==5.1.1 \ + --hash=sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72 \ + --hash=sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24 + # via safir +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via cookiecutter -scons==4.3.0 \ - --hash=sha256:8c13911a2aa40552553488f7d625af4c0768fc8cdedab4a858d8ce42c8c3664d \ - --hash=sha256:d47081587e3675cc168f1f54f0d74a69b328a2fc90ec4feb85f728677419b879 +rubin-squarebot==0.10.0 \ + --hash=sha256:c532572663df709bf9d48b824531e469038f9582dbd6aa5d13110dfc453da09a \ + --hash=sha256:f5c97d5eba01a7c9c1aa067d24f18cdddf41b0649252d81a698c5021fc2e370b + # via -r requirements/main.in +safir==6.4.0 \ + --hash=sha256:ba7af071eab0d198e6e15a2117028566f3f4237e02e2278e8bfc2633a7c68228 \ + --hash=sha256:f38c3f1d7d76d304984b572288826510e5c7a0e1f965b2eabdd7f3bace07c48a + # via + # -r requirements/main.in + # rubin-squarebot +safir-logging==6.4.0 \ + --hash=sha256:4031a430d738b8fe5bfd29125dce6cbf4e4949879307ba4146648afa3d24cd0a \ + --hash=sha256:e2dbf0b5d9dabecd70c27bff9bf01629bf0724b05b0f0087a1fe4f45c702215f + # via safir +scons==4.8.1 \ + --hash=sha256:5b641357904d2f56f7bfdbb37e165ab996b6143c948b9df0efc7305f54949daa \ + --hash=sha256:a4c3b434330e2d7d975002fd6783284ba348bf394db94c8f83fdc5bf69cdb8d7 # via templatekit six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via python-dateutil -smmap==5.0.0 \ - --hash=sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94 \ - --hash=sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936 +smmap==5.0.1 \ + --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ + --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da # via gitdb -structlog==22.1.0 \ - --hash=sha256:760d37b8839bd4fe1747bed7b80f7f4de160078405f4b6a1db9270ccbfce6c30 \ - --hash=sha256:94b29b1d62b2659db154f67a9379ec1770183933d6115d21f21aa25cfc9a7393 - # via -r requirements/main.in -templatekit==0.5.1 \ - --hash=sha256:13b0574ca06415f02bf222d898e6fa164ecb51b9b3246ea41be0a12b1884d5de \ - --hash=sha256:7a200b1559426de420a63fe8193d48c084d68b915ddaba97b4762ba0219fbd71 +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # anyio + # httpx +starlette==0.38.6 \ + --hash=sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05 \ + --hash=sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead + # via + # -r requirements/main.in + # fastapi + # safir +structlog==24.4.0 \ + --hash=sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610 \ + --hash=sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4 + # via + # safir + # safir-logging +templatekit==0.6.0 \ + --hash=sha256:229d1ad008dfedf43e9cf2cdf33d6eb77610d04839bd0b325dc30cb0e3c039a7 \ + --hash=sha256:a982192f2b96ee49e2f03c389702aafaed3bfc0ad3da06f904259e3d5bd2600c # via -r requirements/main.in text-unidecode==1.3 \ --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 # via python-slugify +types-python-dateutil==2.9.0.20241003 \ + --hash=sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d \ + --hash=sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446 + # via arrow +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # aiokafka + # fastapi + # faststream + # pydantic + # pydantic-core uritemplate==4.1.1 \ --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e - # via - # gidgethub - # kafkit -urllib3==1.26.11 \ - --hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \ - --hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a + # via gidgethub +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 # via requests -yarl==1.7.2 \ - --hash=sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac \ - --hash=sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8 \ - --hash=sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e \ - --hash=sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746 \ - --hash=sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98 \ - --hash=sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125 \ - --hash=sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d \ - --hash=sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d \ - --hash=sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986 \ - --hash=sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d \ - --hash=sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec \ - --hash=sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8 \ - --hash=sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee \ - --hash=sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3 \ - --hash=sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1 \ - --hash=sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd \ - --hash=sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b \ - --hash=sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de \ - --hash=sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0 \ - --hash=sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8 \ - --hash=sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6 \ - --hash=sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245 \ - --hash=sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23 \ - --hash=sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332 \ - --hash=sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1 \ - --hash=sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c \ - --hash=sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4 \ - --hash=sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0 \ - --hash=sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8 \ - --hash=sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832 \ - --hash=sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58 \ - --hash=sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6 \ - --hash=sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1 \ - --hash=sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52 \ - --hash=sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92 \ - --hash=sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185 \ - --hash=sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d \ - --hash=sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d \ - --hash=sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b \ - --hash=sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739 \ - --hash=sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05 \ - --hash=sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63 \ - --hash=sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d \ - --hash=sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa \ - --hash=sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913 \ - --hash=sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe \ - --hash=sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b \ - --hash=sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b \ - --hash=sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656 \ - --hash=sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1 \ - --hash=sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4 \ - --hash=sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e \ - --hash=sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63 \ - --hash=sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271 \ - --hash=sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed \ - --hash=sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d \ - --hash=sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda \ - --hash=sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265 \ - --hash=sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f \ - --hash=sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c \ - --hash=sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba \ - --hash=sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c \ - --hash=sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b \ - --hash=sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523 \ - --hash=sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a \ - --hash=sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef \ - --hash=sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95 \ - --hash=sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72 \ - --hash=sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794 \ - --hash=sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41 \ - --hash=sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576 \ - --hash=sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59 - # via aiohttp - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. -# setuptools +uvicorn==0.31.0 \ + --hash=sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906 \ + --hash=sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced + # via -r requirements/main.in +uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \ + --hash=sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847 \ + --hash=sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2 \ + --hash=sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b \ + --hash=sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315 \ + --hash=sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5 \ + --hash=sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469 \ + --hash=sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d \ + --hash=sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf \ + --hash=sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9 \ + --hash=sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab \ + --hash=sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e \ + --hash=sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e \ + --hash=sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0 \ + --hash=sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756 \ + --hash=sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73 \ + --hash=sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006 \ + --hash=sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541 \ + --hash=sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae \ + --hash=sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a \ + --hash=sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996 \ + --hash=sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7 \ + --hash=sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00 \ + --hash=sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b \ + --hash=sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10 \ + --hash=sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95 \ + --hash=sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9 \ + --hash=sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037 \ + --hash=sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6 \ + --hash=sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66 \ + --hash=sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba \ + --hash=sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf + # via uvicorn +watchfiles==0.24.0 \ + --hash=sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a \ + --hash=sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22 \ + --hash=sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a \ + --hash=sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0 \ + --hash=sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827 \ + --hash=sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1 \ + --hash=sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c \ + --hash=sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e \ + --hash=sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188 \ + --hash=sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b \ + --hash=sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5 \ + --hash=sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90 \ + --hash=sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef \ + --hash=sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b \ + --hash=sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15 \ + --hash=sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48 \ + --hash=sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e \ + --hash=sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df \ + --hash=sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd \ + --hash=sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91 \ + --hash=sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d \ + --hash=sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e \ + --hash=sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4 \ + --hash=sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a \ + --hash=sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370 \ + --hash=sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1 \ + --hash=sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea \ + --hash=sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04 \ + --hash=sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896 \ + --hash=sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f \ + --hash=sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f \ + --hash=sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43 \ + --hash=sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735 \ + --hash=sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da \ + --hash=sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a \ + --hash=sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61 \ + --hash=sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3 \ + --hash=sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c \ + --hash=sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f \ + --hash=sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361 \ + --hash=sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855 \ + --hash=sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327 \ + --hash=sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5 \ + --hash=sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab \ + --hash=sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633 \ + --hash=sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777 \ + --hash=sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b \ + --hash=sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be \ + --hash=sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f \ + --hash=sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b \ + --hash=sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e \ + --hash=sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b \ + --hash=sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366 \ + --hash=sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823 \ + --hash=sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3 \ + --hash=sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1 \ + --hash=sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f \ + --hash=sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418 \ + --hash=sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886 \ + --hash=sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571 \ + --hash=sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c \ + --hash=sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94 \ + --hash=sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428 \ + --hash=sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234 \ + --hash=sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6 \ + --hash=sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968 \ + --hash=sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9 \ + --hash=sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c \ + --hash=sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e \ + --hash=sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab \ + --hash=sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec \ + --hash=sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444 \ + --hash=sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b \ + --hash=sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c \ + --hash=sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca \ + --hash=sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b \ + --hash=sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18 \ + --hash=sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318 \ + --hash=sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07 \ + --hash=sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430 \ + --hash=sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c \ + --hash=sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83 \ + --hash=sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05 + # via uvicorn +websockets==13.1 \ + --hash=sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a \ + --hash=sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54 \ + --hash=sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23 \ + --hash=sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7 \ + --hash=sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135 \ + --hash=sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700 \ + --hash=sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf \ + --hash=sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5 \ + --hash=sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e \ + --hash=sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c \ + --hash=sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02 \ + --hash=sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a \ + --hash=sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418 \ + --hash=sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f \ + --hash=sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3 \ + --hash=sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68 \ + --hash=sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978 \ + --hash=sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20 \ + --hash=sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295 \ + --hash=sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b \ + --hash=sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6 \ + --hash=sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb \ + --hash=sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a \ + --hash=sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa \ + --hash=sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0 \ + --hash=sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a \ + --hash=sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238 \ + --hash=sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c \ + --hash=sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084 \ + --hash=sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19 \ + --hash=sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d \ + --hash=sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7 \ + --hash=sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9 \ + --hash=sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79 \ + --hash=sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96 \ + --hash=sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6 \ + --hash=sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe \ + --hash=sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842 \ + --hash=sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa \ + --hash=sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3 \ + --hash=sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d \ + --hash=sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51 \ + --hash=sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7 \ + --hash=sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09 \ + --hash=sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096 \ + --hash=sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9 \ + --hash=sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b \ + --hash=sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5 \ + --hash=sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678 \ + --hash=sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea \ + --hash=sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d \ + --hash=sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49 \ + --hash=sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc \ + --hash=sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5 \ + --hash=sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027 \ + --hash=sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0 \ + --hash=sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878 \ + --hash=sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c \ + --hash=sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa \ + --hash=sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f \ + --hash=sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6 \ + --hash=sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2 \ + --hash=sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf \ + --hash=sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708 \ + --hash=sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6 \ + --hash=sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f \ + --hash=sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd \ + --hash=sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2 \ + --hash=sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d \ + --hash=sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7 \ + --hash=sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f \ + --hash=sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5 \ + --hash=sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6 \ + --hash=sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557 \ + --hash=sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14 \ + --hash=sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7 \ + --hash=sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd \ + --hash=sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c \ + --hash=sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17 \ + --hash=sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23 \ + --hash=sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db \ + --hash=sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6 \ + --hash=sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d \ + --hash=sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9 \ + --hash=sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee \ + --hash=sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6 + # via uvicorn diff --git a/ruff-shared.toml b/ruff-shared.toml new file mode 100644 index 0000000..aff2c6e --- /dev/null +++ b/ruff-shared.toml @@ -0,0 +1,144 @@ +# Generic shared Ruff configuration file. It should be possible to use this +# file unmodified in different packages provided that one likes the style that +# it enforces. +# +# This file should be used from pyproject.toml as follows: +# +# [tool.ruff] +# extend = "ruff-shared.toml" +# +# It can then be extended with project-specific rules. A common additional +# setting in pyproject.toml is tool.ruff.lint.extend-per-file-ignores, to add +# additional project-specific ignore rules for specific paths. +# +# The rule used with Ruff configuration is to disable every non-deprecated +# lint rule that has legitimate exceptions that are not dodgy code, rather +# than cluttering code with noqa markers. This is therefore a reiatively +# relaxed configuration that errs on the side of disabling legitimate rules. +# +# Reference for settings: https://docs.astral.sh/ruff/settings/ +# Reference for rules: https://docs.astral.sh/ruff/rules/ +exclude = ["docs/**"] +line-length = 79 +target-version = "py312" + +[format] +docstring-code-format = true + +[lint] +ignore = [ + "ANN401", # sometimes Any is the right type + "ARG001", # unused function arguments are often legitimate + "ARG002", # unused method arguments are often legitimate + "ARG003", # unused class method arguments are often legitimate + "ARG005", # unused lambda arguments are often legitimate + "ASYNC109", # many async functions use asyncio.timeout internally + "BLE001", # we want to catch and report Exception in background tasks + "C414", # nested sorted is how you sort by multiple keys with reverse + "D102", # sometimes we use docstring inheritence + "D104", # don't see the point of documenting every package + "D105", # our style doesn't require docstrings for magic methods + "D106", # Pydantic uses a nested Config class that doesn't warrant docs + "D205", # our documentation style allows a folded first line + "EM101", # justification (duplicate string in traceback) is silly + "EM102", # justification (duplicate string in traceback) is silly + "FBT003", # positional booleans are normal for Pydantic field defaults + "FIX002", # point of a TODO comment is that we're not ready to fix it + "PD011", # attempts to enforce pandas conventions for all data types + "G004", # forbidding logging f-strings is appealing, but not our style + "RET505", # disagree that omitting else always makes code more readable + "PLR0911", # often many returns is clearer and simpler style + "PLR0913", # factory pattern uses constructors with many arguments + "PLR2004", # too aggressive about magic values + "PLW0603", # yes global is discouraged but if needed, it's needed + "S105", # good idea but too many false positives on non-passwords + "S106", # good idea but too many false positives on non-passwords + "S107", # good idea but too many false positives on non-passwords + "S603", # not going to manually mark every subprocess call as reviewed + "S607", # using PATH is not a security vulnerability + "SIM102", # sometimes the formatting of nested if statements is clearer + "SIM117", # sometimes nested with contexts are clearer + "TCH001", # we decided to not maintain separate TYPE_CHECKING blocks + "TCH002", # we decided to not maintain separate TYPE_CHECKING blocks + "TCH003", # we decided to not maintain separate TYPE_CHECKING blocks + "TD003", # we don't require issues be created for TODOs + "TID252", # if we're going to use relative imports, use them always + "TRY003", # good general advice but lint is way too aggressive + "TRY301", # sometimes raising exceptions inside try is the best flow + "UP040", # PEP 695 type aliases not yet supported by mypy + + # The following settings should be disabled when using ruff format + # per https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", +] +select = ["ALL"] + +[lint.per-file-ignores] +"alembic/**" = [ + "INP001", # Alembic files are magical + "D103", # Alembic methods do not need docstrings + "D400", # Alembic migrations have their own docstring format +] +"noxfile.py" = [ + "T201", # print makes sense as output from nox rules +] +"src/*/handlers/**" = [ + "D103", # FastAPI handlers should not have docstrings +] +"*/src/*/handlers/**" = [ + "D103", # FastAPI handlers should not have docstrings +] +"tests/**" = [ + "C901", # tests are allowed to be complex, sometimes that's convenient + "D101", # tests don't need docstrings + "D103", # tests don't need docstrings + "PLR0915", # tests are allowed to be long, sometimes that's convenient + "PT012", # way too aggressive about limiting pytest.raises blocks + "S101", # tests should use assert + "S106", # tests are allowed to hard-code dummy passwords + "S301", # allow tests for whether code can be pickled + "SLF001", # tests are allowed to access private members +] +"*/tests/**" = [ + "C901", # tests are allowed to be complex, sometimes that's convenient + "D101", # tests don't need docstrings + "D103", # tests don't need docstrings + "PLR0915", # tests are allowed to be long, sometimes that's convenient + "PT012", # way too aggressive about limiting pytest.raises blocks + "S101", # tests should use assert + "S106", # tests are allowed to hard-code dummy passwords + "S301", # allow tests for whether code can be pickled + "SLF001", # tests are allowed to access private members +] + +# These are too useful as attributes or methods to allow the conflict with the +# built-in to rule out their use. +[lint.flake8-builtins] +builtins-ignorelist = [ + "all", + "any", + "help", + "id", + "list", + "type", +] + +[lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[lint.pydocstyle] +convention = "numpy" diff --git a/scripts/docker-tag.sh b/scripts/docker-tag.sh deleted file mode 100755 index 080a9c9..0000000 --- a/scripts/docker-tag.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Determine the tag for Docker images based on GitHub Actions environment -# variables. - -set -eo pipefail - -if [ -n "$GITHUB_HEAD_REF" ]; then - # For pull requests - echo ${GITHUB_HEAD_REF} | sed -E 's,/,-,g' -else - # For push events - echo ${GITHUB_REF} | sed -E 's,refs/(heads|tags)/,,' | sed -E 's,/,-,g' -fi diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ae04422..0000000 --- a/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -[metadata] -name = templatebot -description = Templatebot is a Roundtable microservice for creating new projects and files from templates. -author = Association of Universities for Research in Astronomy, Inc. (AURA) -author_email = sqre-admin@lists.lsst.org -long_description = file: README.rst, CHANGELOG.rst, LICENSE -long_description_content_type = text/x-rst -license = MIT -url = https://github.com/lsst-sqre/templatebot -project_urls = - Change log = https://github.com/lsst-sqre/templatebot/master/blob/CHANGELOG.rst - Source code = https://github.com/lsst-sqre/templatebot - Issue tracker = https://github.com/lsst-sqre/templatebot/issues -classifiers = - Development Status :: 4 - Beta - License :: OSI Approved :: MIT License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.9 - Natural Language :: English - Operating System :: POSIX -keywords = - lsst - -[options] -zip_safe = False -include_package_data = True -packages=find: -package_dir = - = src -python_requires = >=3.8 -setup_requires = - setuptools_scm -# Use requirements/main.in for runtime dependencies instead of install_requires - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - templatebot = templatebot.cli:main - -[flake8] -max-line-length = 79 -# E203: whitespace before :, flake8 disagrees with PEP-8 -# W503: line break after binary operator, flake8 disagrees with PEP-8 -ignore = E203, W503 - -[mypy] -disallow_untyped_defs = True -disallow_incomplete_defs = True -ignore_missing_imports = True -show_error_codes = True -strict_equality = True -warn_redundant_casts = True -warn_unreachable = True -warn_unused_ignores = True diff --git a/setup.py b/setup.py deleted file mode 100644 index d5d43d7..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup(use_scm_version=True) diff --git a/src/templatebot/app.py b/src/templatebot/app.py deleted file mode 100644 index a1e9354..0000000 --- a/src/templatebot/app.py +++ /dev/null @@ -1,331 +0,0 @@ -"""Application factory for the aiohttp.web-based app.""" - -import asyncio -import logging -import ssl -import sys -from pathlib import Path - -import cachetools -import structlog -from aiohttp import ClientSession, web -from aiokafka import AIOKafkaProducer -from gidgethub.aiohttp import GitHubAPI -from kafkit.registry.aiohttp import RegistryApi - -from .config import create_config -from .events.avro import Serializer -from .events.router import consume_events -from .events.topics import configure_topics -from .middleware import setup_middleware -from .repo import RepoManager -from .routes import init_root_routes, init_routes -from .slack import consume_kafka - -__all__ = ["create_app"] - - -def create_app(): - """Create the aiohttp.web application.""" - config = create_config() - configure_logging( - profile=config["api.lsst.codes/profile"], - log_level=config["api.lsst.codes/logLevel"], - logger_name=config["api.lsst.codes/loggerName"], - ) - - root_app = web.Application() - root_app.update(config) - root_app.add_routes(init_root_routes()) - root_app.cleanup_ctx.append(init_http_session) - root_app.cleanup_ctx.append(init_gidgethub_session) - - # Create sub-app for the app's public APIs at the correct prefix - prefix = "/" + root_app["api.lsst.codes/name"] - app = web.Application() - setup_middleware(app) - app.add_routes(init_routes()) - app["root"] = root_app # to make the root app's configs available - app.cleanup_ctx.append(init_repo_manager) - app.cleanup_ctx.append(init_serializer) - app.cleanup_ctx.append(configure_kafka_ssl) - if root_app["templatebot/enableTopicConfig"]: - app.cleanup_ctx.append(init_topics) - app.cleanup_ctx.append(init_producer) - if root_app["templatebot/enableSlackConsumer"]: - app.on_startup.append(start_slack_listener) - app.on_cleanup.append(stop_slack_listener) - if root_app["templatebot/enableEventsConsumer"]: - app.on_startup.append(start_events_listener) - app.on_cleanup.append(stop_events_listener) - root_app.add_subapp(prefix, app) - - logger = structlog.get_logger(root_app["api.lsst.codes/loggerName"]) - logger.info("Started templatebot") - - return root_app - - -def configure_logging( - profile="development", log_level="info", logger_name="templatebot" -): - """Configure logging and structlog.""" - stream_handler = logging.StreamHandler(stream=sys.stdout) - stream_handler.setFormatter(logging.Formatter("%(message)s")) - logger = logging.getLogger(logger_name) - logger.addHandler(stream_handler) - logger.setLevel(log_level.upper()) - - if profile == "production": - # JSON-formatted logging - processors = [ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), - structlog.processors.JSONRenderer(), - ] - else: - # Key-value formatted logging - processors = [ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), - structlog.dev.ConsoleRenderer(), - ] - - structlog.configure( - processors=processors, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) - - -async def init_http_session(app): - """Create an aiohttp.ClientSession and make it available as a - ``'api.lsst.codes/httpSession'`` key on the application. - - Notes - ----- - Use this function as a `cleanup context`_: - - .. code-block:: python - - python.cleanup_ctx.append(init_http_session) - - The session is automatically closed on shut down. - - Access the session: - - .. code-block:: python - - session = app['api.lsst.codes/httpSession'] - - .. cleanup context: - https://aiohttp.readthedocs.io/en/stable/web_reference.html#aiohttp.web.Application.cleanup_ctx - """ - # Startup phase - session = ClientSession() - app["api.lsst.codes/httpSession"] = session - yield - - # Cleanup phase - await app["api.lsst.codes/httpSession"].close() - - -async def init_gidgethub_session(app): - """Create a Gidgethub client session to access the GitHub api. - - Notes - ----- - Use this function as a cleanup content. - - Access the client as ``app['templatebot/gidgethub']``. - """ - session = app["api.lsst.codes/httpSession"] - token = app["templatebot/githubToken"] - username = app["templatebot/githubUsername"] - cache = cachetools.LRUCache(maxsize=500) - gh = GitHubAPI(session, username, oauth_token=token, cache=cache) - app["templatebot/gidgethub"] = gh - - yield - - # No cleanup to do - - -async def configure_kafka_ssl(app): - """Configure an SSL context for the Kafka client (if appropriate). - - Notes - ----- - Use this function as a `cleanup context`_: - - .. code-block:: python - - app.cleanup_ctx.append(init_http_session) - """ - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - - ssl_context_key = "templatebot/kafkaSslContext" - - if app["root"]["templatebot/kafkaProtocol"] != "SSL": - app["root"][ssl_context_key] = None - return - - cluster_ca_cert_path = app["root"]["templatebot/clusterCaPath"] - client_ca_cert_path = app["root"]["templatebot/clientCaPath"] - client_cert_path = app["root"]["templatebot/clientCertPath"] - client_key_path = app["root"]["templatebot/clientKeyPath"] - - if cluster_ca_cert_path is None: - raise RuntimeError("Kafka protocol is SSL but cluster CA is not set") - if client_cert_path is None: - raise RuntimeError("Kafka protocol is SSL but client cert is not set") - if client_key_path is None: - raise RuntimeError("Kafka protocol is SSL but client key is not set") - - if client_ca_cert_path is not None: - logger.info("Contatenating Kafka client CA and certificate files.") - # Need to contatenate the client cert and CA certificates. This is - # typical for Strimzi-based Kafka clusters. - client_ca = Path(client_ca_cert_path).read_text() - client_cert = Path(client_cert_path).read_text() - new_client_cert = "\n".join([client_cert, client_ca]) - new_client_cert_path = ( - app["root"]["templatebot/certCacheDir"] / "client.crt" - ) - new_client_cert_path.write_text(new_client_cert) - client_cert_path = str(new_client_cert_path) - - # Create a SSL context on the basis that we're the client authenticating - # the server (the Kafka broker). - ssl_context = ssl.create_default_context( - purpose=ssl.Purpose.SERVER_AUTH, cafile=cluster_ca_cert_path - ) - # Add the certificates that the Kafka broker uses to authenticate us. - ssl_context.load_cert_chain( - certfile=client_cert_path, keyfile=client_key_path - ) - app["root"][ssl_context_key] = ssl_context - - logger.info("Created Kafka SSL context") - - yield - - -async def start_slack_listener(app): - """Start the Kafka consumer as a background task (``on_startup`` signal - handler). - """ - app["kafka_consumer_task"] = app.loop.create_task(consume_kafka(app)) - - -async def stop_slack_listener(app): - """Stop the Kafka consumer (``on_cleanup`` signal handler).""" - app["kafka_consumer_task"].cancel() - await app["kafka_consumer_task"] - - -async def init_repo_manager(app): - """Create and cleanup the RepoManager.""" - manager = RepoManager( - url=app["root"]["templatebot/repoUrl"], - cache_dir=app["root"]["templatebot/repoCachePath"], - logger=structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]), - ) - manager.clone(gitref=app["root"]["templatebot/repoRef"]) - app["templatebot/repo"] = manager - - yield - - app["templatebot/repo"].delete_all() - - -async def init_serializer(app): - """Init the Avro serializer for SQuaRE Events.""" - # Start up phase - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - logger.info("Setting up Avro serializers") - - registry = RegistryApi( - session=app["root"]["api.lsst.codes/httpSession"], - url=app["root"]["templatebot/registryUrl"], - ) - - serializer = await Serializer.setup(registry=registry, app=app) - app["templatebot/eventSerializer"] = serializer - logger.info("Finished setting up Avro serializer for Slack events") - - yield - - -async def init_topics(app): - """Initialize Kafka topics for SQuaRE Events.""" - # Start up phase - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - logger.info("Setting up templatebot Kafka topics") - - configure_topics(app) - - yield - - -async def start_events_listener(app): - """Start the Kafka consumer for templatebot events as a background task - (``on_startup`` signal handler). - """ - app["templatebot/events_consumer_task"] = app.loop.create_task( - consume_events(app) - ) - - -async def stop_events_listener(app): - """Stop the Kafka consumer for templatebot events (``on_cleanup`` signal - handler). - """ - app["templatebot/events_consumer_task"].cancel() - await app["templatebot/events_consumer_task"] - - -async def init_producer(app): - """Initialize and cleanup the aiokafka Producer instance - - Notes - ----- - Use this function as a cleanup context, see - https://aiohttp.readthedocs.io/en/stable/web_reference.html#aiohttp.web.Application.cleanup_ctx - - To access the producer: - - .. code-block:: python - - producer = app['templatebot/producer'] - """ - # Startup phase - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - logger.info("Starting Kafka producer") - loop = asyncio.get_running_loop() - producer = AIOKafkaProducer( - loop=loop, - bootstrap_servers=app["root"]["templatebot/brokerUrl"], - ssl_context=app["root"]["templatebot/kafkaSslContext"], - security_protocol=app["root"]["templatebot/kafkaProtocol"], - ) - await producer.start() - app["templatebot/producer"] = producer - logger.info("Finished starting Kafka producer") - - yield - - # cleanup phase - logger.info("Shutting down Kafka producer") - await producer.stop() diff --git a/src/templatebot/cli.py b/src/templatebot/cli.py deleted file mode 100644 index 86ff3ff..0000000 --- a/src/templatebot/cli.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Administrative command line interface.""" - -import click -from aiohttp.web import run_app - -from templatebot.app import create_app - -__all__ = ["main", "help", "run"] - -# Add -h as a help shortcut option -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -@click.group(context_settings=CONTEXT_SETTINGS) -@click.version_option(message="%(version)s") -@click.pass_context -def main(ctx): - """templatebot - - Admin and dev commands for templatebot. - """ - # Subcommands should use the click.pass_obj decorator to get this - # ctx object as the first argument. - ctx.obj = {} - - -@main.command() -@click.argument("topic", default=None, required=False, nargs=1) -@click.pass_context -def help(ctx, topic, **kw): - """Show help for any command.""" - # The help command implementation is taken from - # https://www.burgundywall.com/post/having-click-help-subcommand - if topic is None: - click.echo(ctx.parent.get_help()) - else: - click.echo(main.commands[topic].get_help(ctx)) - - -@main.command() -@click.option( - "--port", default=8080, type=int, help="Port to run the application on." -) -@click.pass_context -def run(ctx, port): - """Run the application (for production).""" - app = create_app() - run_app(app, port=port) diff --git a/src/templatebot/config.py b/src/templatebot/config.py index 4537de4..0b8f859 100644 --- a/src/templatebot/config.py +++ b/src/templatebot/config.py @@ -1,144 +1,312 @@ -"""Configuration collection.""" +"""Application settings.""" -import os +from __future__ import annotations + +import ssl +from enum import Enum from pathlib import Path -__all__ = ["create_config"] +from pydantic import DirectoryPath, Field, FilePath, HttpUrl, SecretStr +from pydantic_settings import BaseSettings, SettingsConfigDict +from safir.logging import LogLevel, Profile + +__all__ = ["Config", "config"] + + +class KafkaSecurityProtocol(str, Enum): + """Kafka security protocols understood by aiokafka.""" + + PLAINTEXT = "PLAINTEXT" + """Plain-text connection.""" + + SSL = "SSL" + """TLS-encrypted connection.""" -def create_config(): - """Create a config mapping from defaults and environment variable - overrides. +class KafkaSaslMechanism(str, Enum): + """Kafka SASL mechanisms understood by aiokafka.""" - Returns - ------- - c : `dict` - A configuration dictionary. + PLAIN = "PLAIN" + """Plain-text SASL mechanism.""" - Examples - -------- - Apply the configuration to the aiohttp.web application:: + SCRAM_SHA_256 = "SCRAM-SHA-256" + """SCRAM-SHA-256 SASL mechanism.""" - app = web.Application() - app.update(create_config) - """ - c = {} + SCRAM_SHA_512 = "SCRAM-SHA-512" + """SCRAM-SHA-512 SASL mechanism.""" - # Application run profile. 'development' or 'production' - c["api.lsst.codes/profile"] = os.getenv( - "API_LSST_CODES_PROFILE", "development" - ).lower() - # That name of the api.lsst.codes service, which is also the root path - # that the app's API is served from. - c["api.lsst.codes/name"] = os.getenv("API_LSST_CODES_NAME", "templatebot") +class KafkaConnectionSettings(BaseSettings): + """Settings for connecting to Kafka.""" - # The name of the logger, which should also be the name of the Python - # package. - c["api.lsst.codes/loggerName"] = os.getenv( - "API_LSST_CODES_LOGGER_NAME", "templatebot" + bootstrap_servers: str = Field( + ..., + title="Kafka bootstrap servers", + description=( + "A comma-separated list of Kafka brokers to connect to. " + "This should be a list of hostnames or IP addresses, " + "each optionally followed by a port number, separated by " + "commas. " + "For example: `kafka-1:9092,kafka-2:9092,kafka-3:9092`." + ), ) - # Log level (INFO or DEBUG) - c["api.lsst.codes/logLevel"] = os.getenv( - "API_LSST_CODES_LOG_LEVEL", - "info" if c["api.lsst.codes/profile"] == "production" else "debug", - ).upper() + security_protocol: KafkaSecurityProtocol = Field( + KafkaSecurityProtocol.PLAINTEXT, + description="The security protocol to use when connecting to Kafka.", + ) - # Path of the repository cache - c["templatebot/repoCachePath"] = Path( - os.getenv("TEMPLATEBOT_CACHE_PATH", ".templatebot_repos") + cert_temp_dir: DirectoryPath | None = Field( + None, + description=( + "Temporary writable directory for concatenating certificates." + ), ) - c["templatebot/certCacheDir"] = Path( - os.getenv("TEMPLATEBOT_CERT_CACHE", ".") + cluster_ca_path: FilePath | None = Field( + None, + title="Path to CA certificate file", + description=( + "The path to the CA certificate file to use for verifying the " + "broker's certificate. " + "This is only needed if the broker's certificate is not signed " + "by a CA trusted by the operating system." + ), ) - # Schema Registry hostname (use same config variable as SQRBOTJR) - c["templatebot/registryUrl"] = os.getenv("REGISTRY_URL") + client_ca_path: FilePath | None = Field( + None, + title="Path to client CA certificate file", + description=( + "The path to the client CA certificate file to use for " + "authentication. " + "This is only needed when the client certificate needs to be" + "concatenated with the client CA certificate, which is common" + "for Strimzi installations." + ), + ) - # Kafka broker host (use same config variable as SQRBOTJR) - c["templatebot/brokerUrl"] = os.getenv("KAFKA_BROKER") + client_cert_path: FilePath | None = Field( + None, + title="Path to client certificate file", + description=( + "The path to the client certificate file to use for " + "authentication. " + "This is only needed if the broker is configured to require " + "SSL client authentication." + ), + ) - # Kafka security protocol: PLAINTEXT or SSL - c["templatebot/kafkaProtocol"] = os.getenv("KAFKA_PROTOCOL") + client_key_path: FilePath | None = Field( + None, + title="Path to client key file", + description=( + "The path to the client key file to use for authentication. " + "This is only needed if the broker is configured to require " + "SSL client authentication." + ), + ) - # Kafka SSL configuration (optional) - c["templatebot/clusterCaPath"] = os.getenv("KAFKA_CLUSTER_CA") - c["templatebot/clientCaPath"] = os.getenv("KAFKA_CLIENT_CA") - c["templatebot/clientCertPath"] = os.getenv("KAFKA_CLIENT_CERT") - c["templatebot/clientKeyPath"] = os.getenv("KAFKA_CLIENT_KEY") + client_key_password: SecretStr | None = Field( + None, + title="Password for client key file", + description=( + "The password to use for decrypting the client key file. " + "This is only needed if the client key file is encrypted." + ), + ) - # Slack token (use same config variable as SQRBOTJR) - c["templatebot/slackToken"] = os.getenv("SLACK_TOKEN") + sasl_mechanism: KafkaSaslMechanism | None = Field( + KafkaSaslMechanism.PLAIN, + title="SASL mechanism", + description=( + "The SASL mechanism to use for authentication. " + "This is only needed if SASL authentication is enabled." + ), + ) - # Suffix to add to Schema Registry suffix names. This is useful when - # deploying sqrbot-jr for testing/staging and you do not want to affect - # the production subject and its compatibility lineage. - c["templatebot/subjectSuffix"] = os.getenv( - "TEMPLATEBOT_SUBJECT_SUFFIX", "" + sasl_username: str | None = Field( + None, + title="SASL username", + description=( + "The username to use for SASL authentication. " + "This is only needed if SASL authentication is enabled." + ), ) - # Compatibility level to apply to Schema Registry subjects. Use - # NONE for testing and development, but prefer FORWARD_TRANSITIVE for - # production. - c["templatebot/subjectCompatibility"] = os.getenv( - "TEMPLATEBOT_SUBJECT_COMPATIBILITY", "FORWARD_TRANSITIVE" + sasl_password: SecretStr | None = Field( + None, + title="SASL password", + description=( + "The password to use for SASL authentication. " + "This is only needed if SASL authentication is enabled." + ), ) - # Template repository (Git URL) - c["templatebot/repoUrl"] = os.getenv( - "TEMPLATEBOT_REPO", "https://github.com/lsst/templates" + model_config = SettingsConfigDict( + env_prefix="KAFKA_", case_sensitive=False ) - # Default Git ref for the template repository ('templatebot/repo') - c["templatebot/repoRef"] = os.getenv("TEMPLATEBOT_REPO_REF", "main") + @property + def ssl_context(self) -> ssl.SSLContext | None: + """An SSL context for connecting to Kafka with aiokafka, if the + Kafka connection is configured to use SSL. + """ + if ( + self.security_protocol != KafkaSecurityProtocol.SSL + or self.cluster_ca_path is None + or self.client_cert_path is None + or self.client_key_path is None + ): + return None + + client_cert_path = Path(self.client_cert_path) + + if self.client_ca_path is not None: + # Need to contatenate the client cert and CA certificates. This is + # typical for Strimzi-based Kafka clusters. + if self.cert_temp_dir is None: + raise RuntimeError( + "KAFKIT_KAFKA_CERT_TEMP_DIR must be set when " + "a client CA certificate is provided." + ) + client_ca = Path(self.client_ca_path).read_text() + client_cert = Path(self.client_cert_path).read_text() + sep = "" if client_ca.endswith("\n") else "\n" + new_client_cert = sep.join([client_cert, client_ca]) + new_client_cert_path = Path(self.cert_temp_dir) / "client.crt" + new_client_cert_path.write_text(new_client_cert) + client_cert_path = Path(new_client_cert_path) + + # Create an SSL context on the basis that we're the client + # authenticating the server (the Kafka broker). + ssl_context = ssl.create_default_context( + purpose=ssl.Purpose.SERVER_AUTH, cafile=str(self.cluster_ca_path) + ) + # Add the certificates that the Kafka broker uses to authenticate us. + ssl_context.load_cert_chain( + certfile=str(client_cert_path), keyfile=str(self.client_key_path) + ) + + return ssl_context + - # GitHub token for SQuaRE bot - c["templatebot/githubToken"] = os.getenv("TEMPLATEBOT_GITHUB_TOKEN") - c["templatebot/githubUsername"] = os.getenv("TEMPLATEBOT_GITHUB_USER") +class Config(BaseSettings): + """Configuration for templatebot.""" - # Topic names - c["templatebot/prerenderTopic"] = os.getenv( - "TEMPLATEBOT_TOPIC_PRERENDER", "templatebot.prerender" + name: str = Field("templatebot", title="Name of application") + + path_prefix: str = Field( + "/templatebot", title="URL prefix for application" ) - c["templatebot/renderreadyTopic"] = os.getenv( - "TEMPLATEBOT_TOPIC_RENDERREADY", "templatebot.render-ready" + + profile: Profile = Field( + Profile.development, title="Application logging profile" ) - c["templatebot/postrenderTopic"] = os.getenv( - "TEMPLATEBOT_TOPIC_POSTRENDER", "templatebot.postrender" + + log_level: LogLevel = Field( + LogLevel.INFO, title="Log level of the application's logger" ) - c["templatebot/appMentionTopic"] = os.getenv( - "SQRBOTJR_TOPIC_APP_MENTION", "sqrbot.app.mention" + + environment_url: str = Field( + ..., + title="Environment URL", + examples=["https://roundtable.lsst.cloud"], ) - c["templatebot/messageImTopic"] = os.getenv( - "SQRBOTJR_TOPIC_MESSAGE_IM", "sqrbot.message.im" + + kafka: KafkaConnectionSettings = Field( + default_factory=KafkaConnectionSettings, + title="Kafka connection configuration.", ) - c["templatebot/interactionTopic"] = os.getenv( - "SQRBOTJR_TOPIC_INTERACTION", "sqrbot.interaction" + + ltd_username: str = Field( + ..., + description="The username for the LSST the Docs API.", ) - # Group IDs for the Slack topic consumer and the the templatebot - # consumer; defaults to the app's name. - c["templatebot/slackGroupId"] = os.getenv( - "TEMPLATEBOT_SLACK_GROUP_ID", c["api.lsst.codes/name"] + ltd_password: SecretStr = Field( + ..., + description="The password for the LSST the Docs API user.", ) - c["templatebot/eventsGroupId"] = os.getenv( - "TEMPLATEBOT_EVENTS_GROUP_ID", c["api.lsst.codes/name"] + + github_app_id: int = Field( + ..., + description=( + "The GitHub App ID, as determined by GitHub when setting up a " + "GitHub App." + ), ) - # Enable topic configuration by the app (disable is its being configured - # externally). - c["templatebot/enableTopicConfig"] = bool( - int(os.getenv("TEMPLATEBOT_TOPIC_CONFIG", "1")) + github_app_private_key: SecretStr = Field( + ..., description="The GitHub app private key." ) - # Enable the Kafka consumer listening to sqrbot - c["templatebot/enableSlackConsumer"] = bool( - int(os.getenv("TEMPLATEBOT_ENABLE_SLACK_CONSUMER", "1")) + + github_username: str = Field( + "squarebot[bot]", + description=( + "Username, used as the name for GitHub commits " + "the email address if `commit_email` is not set " + "case, must be the same as the login attribute in the GitHub" + "users API." + ), ) - # Enable the Kafka consumer listening to events from the aide - c["templatebot/enableEventsConsumer"] = bool( - int(os.getenv("TEMPLATEBOT_ENABLE_EVENTS_CONSUMER", "1")) + + slack_token: SecretStr = Field(title="Slack bot token") + + slack_app_id: str = Field(title="Slack app ID") + + template_repo_url: HttpUrl = Field( + description="URL of the template repository" ) - return c + template_cache_dir: Path = Field( + description="Directory where template repositories are cloned.", + ) + + consumer_group_id: str = Field( + "templatebot", + title="Kafka consumer group ID", + description=( + "Each Kafka subscriber has a unique consumer group ID, which " + "uses this configuration as a prefix." + ), + ) + + app_mention_topic: str = Field( + "squarebot.app.mention", + title="app_mention Kafka topic", + description="Kafka topic name for `app_mention` Slack events.", + ) + + message_im_topic: str = Field( + "squarebot.message.im", + title="message.im Kafka topic", + description=( + "Kafka topic name for `message.im` Slack events (direct message " + " channels)." + ), + ) + + block_actions_topic: str = Field( + "squarebot.block-actions", + description=( + "Kafka topic name for Slack block actions interaction " + "Slack events" + ), + ) + + view_submission_topic: str = Field( + "squarebot.view-submission", + description=( + "Kafka topic name for Slack view submission interaction " + "Slack events" + ), + ) + + model_config = SettingsConfigDict( + env_prefix="TEMPLATEBOT_", case_sensitive=False + ) + + +config = Config() +"""Configuration for templatebot.""" diff --git a/src/templatebot/constants.py b/src/templatebot/constants.py new file mode 100644 index 0000000..cd4e2a3 --- /dev/null +++ b/src/templatebot/constants.py @@ -0,0 +1,10 @@ +"""Application constants.""" + +SELECT_PROJECT_TEMPLATE_ACTION = "templatebot_select_project_template" +"""Action ID of the static select menu for selecting a project template.""" + +SELECT_FILE_TEMPLATE_ACTION = "templatebot_select_file_template" +"""Action ID of the static select menu for selecting a file template.""" + +TEMPLATE_VARIABLES_MODAL_CALLBACK_ID = "templatebot_template_variables" +"""Callback ID for the template variables modal.""" diff --git a/src/templatebot/dependencies/__init__.py b/src/templatebot/dependencies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/templatebot/dependencies/consumercontext.py b/src/templatebot/dependencies/consumercontext.py new file mode 100644 index 0000000..3857095 --- /dev/null +++ b/src/templatebot/dependencies/consumercontext.py @@ -0,0 +1,104 @@ +"""A dependency for providing context to consumers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from aiokafka import ConsumerRecord +from faststream import context +from faststream.kafka.fastapi import KafkaMessage +from structlog import get_logger +from structlog.stdlib import BoundLogger + +from ..factory import Factory, ProcessContext + + +@dataclass(slots=True, kw_only=True) +class ConsumerContext: + """Context for consumers.""" + + logger: BoundLogger + """Logger for the consumer.""" + + factory: Factory + """Factory for creating services.""" + + record: ConsumerRecord | None = None + """The Kafka record being processed.""" + + def rebind_logger(self, **values: Any) -> None: + """Add the given values to the logging context. + + Parameters + ---------- + **values + Additional values that should be added to the logging context. + """ + self.logger = self.logger.bind(**values) + self.factory.set_logger(self.logger) + + +class ConsumerContextDependency: + """Provide a per-message context as a dependency for a FastStream consumer. + + Each message handler class gets a `ConsumerContext`. To save overhead, the + portions of the context that are shared by all requests are collected into + the single process-global `~unfurlbot.factory.ProcessContext` and reused + with each request. + """ + + def __init__(self) -> None: + self._process_context: ProcessContext | None = None + + async def __call__(self) -> ConsumerContext: + """Create a per-request context.""" + # Get the message from the FastStream context + message: KafkaMessage = context.get_local("message") + record = message.raw_message + + # Add the Kafka context to the logger + logger = get_logger(__name__) # eventually use a logger dependency + kafka_context = { + "topic": record.topic, # type: ignore [union-attr] + "offset": record.offset, # type: ignore [union-attr] + "partition": record.partition, # type: ignore [union-attr] + } + logger = logger.bind(kafka=kafka_context) + + return ConsumerContext( + logger=logger, + factory=Factory( + logger=logger, process_context=self.process_context + ), + ) + + @property + def process_context(self) -> ProcessContext: + """The underlying process context, primarily for use in tests.""" + if not self._process_context: + raise RuntimeError("ConsumerContextDependency not initialized") + return self._process_context + + async def initialize(self) -> None: + """Initialize the process-wide shared context.""" + if self._process_context: + await self._process_context.aclose() + self._process_context = await ProcessContext.create() + + def create_factory(self, logger: BoundLogger) -> Factory: + """Create a factory for use outside a request context.""" + return Factory( + logger=logger, + process_context=self.process_context, + ) + + async def aclose(self) -> None: + """Clean up the per-process configuration.""" + if self._process_context: + await self._process_context.aclose() + self._process_context = None + + +consumer_context_dependency = ConsumerContextDependency() +"""The dependency that will return the per-request context.""" diff --git a/src/templatebot/events/avro.py b/src/templatebot/events/avro.py deleted file mode 100644 index 6f902e5..0000000 --- a/src/templatebot/events/avro.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Avro serialization and schema management for templatebot.* schemas.""" - -import functools -import json -from pathlib import Path - -import fastavro -import kafkit.registry.errors -import structlog -from kafkit.registry.serializer import PolySerializer - -__all__ = ["Serializer"] - - -class Serializer: - """An Avro (Confluent Wire Format) serializer. - - Always use the `Serializer.setup` method to create a - serializer instance. - - Parameters - ---------- - registry : `kafkit.registry.serializer.PolySerializer` - Client for the Confluent Schema Registry. - logger - Logger instance. - suffix : `str`, optional - If the application is running in a staging environment, this is the - a suffix that can be applied to the Schema subject names. This should - be set through the ``templatebot/subjectSuffix`` configuration key on - the app. Leave as an empty string if the application is not in staging. - """ - - def __init__(self, *, serializer, logger, suffix=""): - self._serializer = serializer - self._logger = logger - self._subject_suffix = suffix - - @classmethod - async def setup(cls, *, registry, app): - """Create a `Serializer` while also registering the schemas and - configuring the associated subjects in the Schema Registry. - - Parameters - ---------- - registry : `kafkit.registry.aiohttp.RegistryApi` - A Schema Registry client. - app : `aiohttp.web.Application` - The application instance. - - Returns - ------- - serializer : `Serializer` - An instance of the serializer. - """ - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - - logger.debug("all schemas", schemas=list_schemas()) - for event_type in list_schemas(): - schema = load_schema( - event_type, suffix=app["root"]["templatebot/subjectSuffix"] - ) - await register_schema(registry, schema, app) - - serializer = PolySerializer(registry=registry) - - return cls( - serializer=serializer, - logger=logger, - suffix=app["root"]["templatebot/subjectSuffix"], - ) - - async def serialize(self, schema_name, message): - """Serialize a Slack event. - - Parameters - ---------- - message : `dict` - The JSON payload for the message. - - Returns - ------- - data : `bytes - Data encoded in the Confluent Wire Format, ready to be sent to a - Kafka broker. - """ - schema = load_schema(schema_name, suffix=self._subject_suffix) - return await self._serializer.serialize(message, schema=schema) - - -@functools.lru_cache() -def load_schema(name, suffix=""): - """Load an Avro schema from the local app data. - - This function is memoized so that repeated calls are fast. - - Parameters - ---------- - name : `str` - Name of the schema. This should be a fully-qualified name that matches - the file name of schemas in the ``schemas/`` directory of the source - repository. - suffix : `str`, optional - A suffix to add to the schema's name. This is typically used to create - "staging" schemas, therefore "staging subjects" in the Schema Registry. - - Returns - ------- - schema : `dict` - A schema object. - """ - schemas_dir = Path(__file__).parent / "schemas" - schema_path = schemas_dir / f"{name}.json" - - schema = json.loads(schema_path.read_text()) - - if suffix: - schema["name"] = "".join((schema["name"], suffix)) - - return fastavro.parse_schema(schema) - - -@functools.lru_cache() -def list_schemas(): - """List the schemas in the local package. - - Returns - ------- - events : `list` [`str`] - List of names of schemas. - - Notes - ----- - This function looks for schema json files in the - ``tempaltebot/events/schemas/events`` directory of the package. - - This function is cached, so repeated calls consume no additional IO. - """ - schemas_dir = Path(__file__).parent / "schemas" - schema_paths = schemas_dir.glob("*.json") - return [p.stem for p in schema_paths] - - -async def register_schema(registry, schema, app): - """Register a schema and configure subject compatibility. - - Parameters - ---------- - registry : `kafkit.registry.aiohttp.RegistryApi` - A Schema Registry client. - schema : `dict` - The Avro schema. Note that the schema should already be versioned with - a staging suffix, if necessary. - app : `aiohttp.web.Application` or `dict` - The application instance, or the application's config dictionary. - - Notes - ----- - This function registers a schema, and then ensures that the associated - subject in the Schema Registry has the appropriate compatibility level - (the ``templatebot/subjectCompatibility`` configuration). - """ - # TODO This function is lifted from sqrbot-jr. Add it to Kafkit? - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - - desired_compat = app["root"]["templatebot/subjectCompatibility"] - - schema_id = await registry.register_schema(schema) - logger.info("Registered schema", subject=schema["name"], id=schema_id) - - subjects = await registry.get("/subjects") - logger.info("All subjects", subjects=subjects) - - subject_name = schema["name"] - - try: - subject_config = await registry.get( - "/config{/subject}", url_vars={"subject": subject_name} - ) - except kafkit.registry.errors.RegistryBadRequestError: - logger.info( - "No existing configuration for this subject.", subject=subject_name - ) - # Create a mock config that forces a reset - subject_config = {"compatibilityLevel": None} - - logger.info("Current subject config", config=subject_config) - if subject_config["compatibilityLevel"] != desired_compat: - await registry.put( - "/config{/subject}", - url_vars={"subject": subject_name}, - data={"compatibility": desired_compat}, - ) - logger.info( - "Reset subject compatibility level", - subject=schema["name"], - compatibility_level=desired_compat, - ) - else: - logger.info( - "Existing subject compatibility level is good", - subject=schema["name"], - compatibility_level=subject_config["compatibilityLevel"], - ) diff --git a/src/templatebot/events/router.py b/src/templatebot/events/router.py deleted file mode 100644 index f345742..0000000 --- a/src/templatebot/events/router.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Router that listens to Kafka topics related to the templatebot aide -that indicates a GitHub repo is ready to populate. -""" - -import asyncio - -import structlog -from aiokafka import AIOKafkaConsumer -from kafkit.registry import Deserializer -from kafkit.registry.aiohttp import RegistryApi - -from .handlers import handle_project_render - -__all__ = ["consume_events"] - - -async def consume_events(app): - """Consume events from templatebot-related topics in SQuaRE Events (Kafka). - - Notes - ----- - Templatebot has *two* Kafka consumers. This is one, and the other is - in `templatebot.slack`. The Slack consumer only listens to topics from - Slack (SQuaRE Bot), and is focused on responding to Slack-based workflows. - This consumer is focused on backend-driven events, such as the - ``templatebot-render_ready`` topic. - """ - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - - registry = RegistryApi( - session=app["root"]["api.lsst.codes/httpSession"], - url=app["root"]["templatebot/registryUrl"], - ) - deserializer = Deserializer(registry=registry) - - consumer_settings = { - "bootstrap_servers": app["root"]["templatebot/brokerUrl"], - "group_id": app["root"]["templatebot/eventsGroupId"], - "auto_offset_reset": "latest", - "ssl_context": app["root"]["templatebot/kafkaSslContext"], - "security_protocol": app["root"]["templatebot/kafkaProtocol"], - } - consumer = AIOKafkaConsumer( - loop=asyncio.get_event_loop(), **consumer_settings - ) - - try: - await consumer.start() - logger.info("Started Kafka consumer for events", **consumer_settings) - - topic_names = [app["root"]["templatebot/renderreadyTopic"]] - logger.info("Subscribing to Kafka topics", names=topic_names) - consumer.subscribe(topic_names) - - partitions = consumer.assignment() - while len(partitions) == 0: - # Wait for the consumer to get partition assignment - await asyncio.sleep(1.0) - partitions = consumer.assignment() - logger.info( - "Initial partition assignment for event topics", - partitions=[str(p) for p in partitions], - ) - - async for message in consumer: - try: - message_info = await deserializer.deserialize( - message.value, include_schema=True - ) - except Exception: - logger.exception( - "Failed to deserialize an event message", - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - continue - - event = message_info["message"] - logger.debug( - "New event message", - topic=message.topic, - partition=message.partition, - offset=message.offset, - contents=event, - ) - - try: - await route_event( - app=app, - event=message_info["message"], - schema_id=message_info["id"], - schema=message_info["schema"], - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - except Exception: - logger.exception( - "Failed to handle event message", - topic=message.topic, - partition=message.partition, - offset=message.offset, - ) - - except asyncio.CancelledError: - logger.info("consume_events task got cancelled") - finally: - logger.info("consume_events task cancelling") - await consumer.stop() - - -async def route_event( - *, event, app, schema_id, schema, topic, partition, offset -): - """Route events from `consume_events` to specific handlers.""" - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - logger = logger.bind( - topic=topic, partition=partition, offset=offset, schema_id=schema_id - ) - - if topic == app["root"]["templatebot/renderreadyTopic"]: - await handle_project_render( - event=event, schema=schema, app=app, logger=logger - ) diff --git a/src/templatebot/events/topics.py b/src/templatebot/events/topics.py deleted file mode 100644 index 954c88a..0000000 --- a/src/templatebot/events/topics.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Kafka topic configuration for Templatebot's own topics.""" - -import structlog -from confluent_kafka.admin import AdminClient, NewTopic - -__all__ = ["configure_topics"] - - -def configure_topics(app): - """Create Kafka topics for templatebot. - - This function is generally called at app startup. - - Parameters - ---------- - app : `aiohttp.web.Application` - The application instance. - - Notes - ----- - This function registers any templatebot-specific topics that don't already - exist. The topics correspond one-to-one with schemas in - ``templatebot/events/schemas/``. - - Access topic names from the root application instance with these keys: - - - ``templatebot/prerenderTopic`` - - ``tempatebot/renderreadyTopic`` - - ``templatebot/postrenderTopic`` - """ - logger = structlog.get_logger(app["root"]["api.lsst.codes/loggerName"]) - - default_num_partitions = 1 - default_replication_factor = 3 - - client = AdminClient( - {"bootstrap.servers": app["root"]["templatebot/brokerUrl"]} - ) - - # Set up topic names - topic_keys = ( - "templatebot/prerenderTopic", - "templatebot/renderreadyTopic", - "templatebot/postrenderTopic", - ) - - # First list existing topics - metadata = client.list_topics(timeout=10) - existing_topic_names = [t for t in metadata.topics.keys()] - - # Create any topics that don't already exist - new_topics = [] - for key in topic_keys: - topic_name = app["root"][key] - if topic_name in existing_topic_names: - topic = metadata.topics[topic_name] - partitions = [p for p in iter(topic.partitions.values())] - logger.info( - "Topic exists", - topic=topic_name, - partitions=len(topic.partitions), - replication_factor=len(partitions[0].replicas), - ) - continue - new_topics.append( - NewTopic( - topic_name, - num_partitions=default_num_partitions, - replication_factor=default_replication_factor, - ) - ) - - if len(new_topics) > 0: - fs = client.create_topics(new_topics) - for topic_name, f in fs.items(): - try: - f.result() # The result itself is None - logger.info( - "Created topic", - topic=topic_name, - partitions=default_num_partitions, - ) - except Exception as e: - logger.error( - "Failed to create topic", topic=topic_name, error=str(e) - ) - raise diff --git a/src/templatebot/factory.py b/src/templatebot/factory.py new file mode 100644 index 0000000..52ce3c4 --- /dev/null +++ b/src/templatebot/factory.py @@ -0,0 +1,149 @@ +"""Factory for templatebot services and other components.""" + +from dataclasses import dataclass +from typing import Self + +import structlog +from httpx import AsyncClient +from structlog.stdlib import BoundLogger + +from templatebot.services.slackblockactions import SlackBlockActionsService +from templatebot.services.slackmessage import SlackMessageService +from templatebot.services.slackview import SlackViewService +from templatebot.services.template import TemplateService +from templatebot.services.templaterepo import TemplateRepoService +from templatebot.storage.githubappclientfactory import GitHubAppClientFactory +from templatebot.storage.ltdclient import LtdClient +from templatebot.storage.repo import RepoManager +from templatebot.storage.slack import SlackWebApiClient + +from .config import config + +__all__ = ["Factory", "ProcessContext"] + + +@dataclass(kw_only=True, frozen=True, slots=True) +class ProcessContext: + """Holds singletons in the context of a Ook process, which might be a + API server or a CLI command. + """ + + http_client: AsyncClient + """Shared HTTP client.""" + + repo_manager: RepoManager + """Template repository manager. This maintains an on-disk cache of + template repository clones. + """ + + @classmethod + async def create(cls) -> Self: + """Create a new process context.""" + http_client = AsyncClient() + repo_manager = RepoManager( + url=str(config.template_repo_url), + cache_dir=config.template_cache_dir, + logger=structlog.get_logger(__name__), + ) + + return cls(http_client=http_client, repo_manager=repo_manager) + + async def aclose(self) -> None: + """Close any resources held by the context.""" + await self.http_client.aclose() + + +class Factory: + """Factory for Squarebot services and other components.""" + + def __init__( + self, + *, + logger: BoundLogger, + process_context: ProcessContext, + ) -> None: + self._process_context = process_context + self._logger = logger + + def set_logger(self, logger: BoundLogger) -> None: + """Reset the logger for the factory. + + This is typically used by the ConsumerContext when values are bound + to the logger. + + Parameters + ---------- + logger + The new logger to use. + """ + self._logger = logger + + def create_slack_web_client(self) -> SlackWebApiClient: + """Create a Slack web API client.""" + return SlackWebApiClient( + http_client=self._process_context.http_client, + token=config.slack_token, + logger=self._logger, + ) + + def create_github_client_factory(self) -> GitHubAppClientFactory: + """Create a new GitHub client factory.""" + return GitHubAppClientFactory( + id=config.github_app_id, + key=config.github_app_private_key.get_secret_value(), + name="templatebot", + http_client=self._process_context.http_client, + ) + + def create_ltd_client(self) -> LtdClient: + """Create a new LSST the Docs client.""" + return LtdClient( + username=config.ltd_username, + password=config.ltd_password, + http_client=self._process_context.http_client, + logger=self._logger, + ) + + def create_slack_message_service(self) -> SlackMessageService: + """Create a new Slack message handling service.""" + return SlackMessageService( + logger=self._logger, + slack_client=self.create_slack_web_client(), + template_repo_service=self.create_template_repo_service(), + ) + + def create_slack_block_actions_service(self) -> SlackBlockActionsService: + """Create a new Slack block actions handling service.""" + return SlackBlockActionsService( + logger=self._logger, + slack_client=self.create_slack_web_client(), + repo_manager=self._process_context.repo_manager, + template_service=self.create_template_service(), + ) + + def create_slack_view_service(self) -> SlackViewService: + """Create a new Slack view handling service.""" + return SlackViewService( + logger=self._logger, + slack_client=self.create_slack_web_client(), + repo_manager=self._process_context.repo_manager, + template_service=self.create_template_service(), + ) + + def create_template_repo_service(self) -> TemplateRepoService: + """Create a new template repository service.""" + return TemplateRepoService( + logger=self._logger, + repo_manager=self._process_context.repo_manager, + slack_client=self.create_slack_web_client(), + ) + + def create_template_service(self) -> TemplateService: + """Create a new template service.""" + return TemplateService( + logger=self._logger, + slack_client=self.create_slack_web_client(), + http_client=self._process_context.http_client, + github_client_factory=self.create_github_client_factory(), + ltd_client=self.create_ltd_client(), + ) diff --git a/src/templatebot/github.py b/src/templatebot/github.py deleted file mode 100644 index 3f3e146..0000000 --- a/src/templatebot/github.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Utilities for working with the GitHub API.""" - -__all__ = ["get_authenticated_user"] - - -async def get_authenticated_user(*, app, logger): - """Get information about the authenticated GitHub user. - - This function wraps the `GET /user - `_ - method. - - Parameters - ---------- - app : `aiohttp.web.Application` - The app instance. - logger - A `structlog` logger instance with bound context related to the - Kafka event. - - Returns - ------- - response : `dict` - The parsed JSON response body from GitHub. - """ - ghclient = app["root"]["templatebot/gidgethub"] - response = await ghclient.getitem("/user") - return response diff --git a/src/templatebot/handlers/__init__.py b/src/templatebot/handlers/__init__.py index a20d57b..e69de29 100644 --- a/src/templatebot/handlers/__init__.py +++ b/src/templatebot/handlers/__init__.py @@ -1 +0,0 @@ -"""Application handlers for requests.""" diff --git a/src/templatebot/handlers/internal/__init__.py b/src/templatebot/handlers/internal/__init__.py new file mode 100644 index 0000000..76ea038 --- /dev/null +++ b/src/templatebot/handlers/internal/__init__.py @@ -0,0 +1,3 @@ +from .endpoints import internal_router + +__all__ = ["internal_router"] diff --git a/src/templatebot/handlers/internal/endpoints.py b/src/templatebot/handlers/internal/endpoints.py new file mode 100644 index 0000000..9a5ec28 --- /dev/null +++ b/src/templatebot/handlers/internal/endpoints.py @@ -0,0 +1,39 @@ +"""Internal HTTP handlers that serve relative to the root path, ``/``. + +These handlers should be used for monitoring, health checks, internal status, +or other information that should not be visible outside the Kubernetes cluster. +These handlers aren't externally visible. +""" + +from fastapi import APIRouter +from safir.metadata import Metadata, get_metadata + +from templatebot.config import config + +__all__ = ["get_index", "internal_router"] + +internal_router = APIRouter() +"""FastAPI router for all internal handlers.""" + + +@internal_router.get( + "/", + description=( + "Return metadata about the running application. Can also be used as" + " a health check. This route is not exposed outside the cluster and" + " therefore cannot be used by external clients." + ), + include_in_schema=False, + response_model=Metadata, + response_model_exclude_none=True, + summary="Application metadata", +) +async def get_index() -> Metadata: + """GET ``/`` (the app's internal root). + + By convention, this endpoint returns only the application's metadata. + """ + return get_metadata( + package_name="templatebot", + application_name=config.name, + ) diff --git a/src/templatebot/handlers/kafka.py b/src/templatebot/handlers/kafka.py new file mode 100644 index 0000000..34324f3 --- /dev/null +++ b/src/templatebot/handlers/kafka.py @@ -0,0 +1,112 @@ +"""Kafka router and consumers.""" + +from typing import Annotated + +from fastapi import Depends +from faststream.kafka.fastapi import KafkaRouter +from faststream.security import BaseSecurity +from rubin.squarebot.models.kafka import ( + SquarebotSlackAppMentionValue, + SquarebotSlackBlockActionsValue, + SquarebotSlackMessageValue, + SquarebotSlackViewSubmissionValue, +) +from structlog import get_logger + +from ..config import config +from ..dependencies.consumercontext import ( + ConsumerContext, + consumer_context_dependency, +) + +__all__ = ["kafka_router", "handle_slack_message"] + + +kafka_security = BaseSecurity(ssl_context=config.kafka.ssl_context) +kafka_router = KafkaRouter( + config.kafka.bootstrap_servers, + security=kafka_security, + logger=get_logger(__name__), +) + + +@kafka_router.subscriber( + config.message_im_topic, + group_id=f"{config.consumer_group_id}-im", +) +async def handle_slack_message( + message: SquarebotSlackMessageValue, + context: Annotated[ConsumerContext, Depends(consumer_context_dependency)], +) -> None: + """Handle a Slack message.""" + logger = context.logger + factory = context.factory + + logger.debug( + "Slack message text", + text=message.text, + ) + + message_service = factory.create_slack_message_service() + await message_service.handle_im_message(message) + + +@kafka_router.subscriber( + config.app_mention_topic, + group_id=f"{config.consumer_group_id}-app-mention", +) +async def handle_slack_app_mention( + message: SquarebotSlackAppMentionValue, + context: Annotated[ConsumerContext, Depends(consumer_context_dependency)], +) -> None: + """Handle a Slack message.""" + logger = context.logger + factory = context.factory + + logger.debug( + "Slack message text", + text=message.text, + ) + + message_service = factory.create_slack_message_service() + await message_service.handle_app_mention(message) + + +@kafka_router.subscriber( + config.block_actions_topic, + group_id=f"{config.consumer_group_id}-block-actions", +) +async def handle_slack_block_actions( + payload: SquarebotSlackBlockActionsValue, + context: Annotated[ConsumerContext, Depends(consumer_context_dependency)], +) -> None: + """Handle a Slack block_actions interaction.""" + logger = context.logger + factory = context.factory + + logger.debug( + "Got Slack block_actions", + actions=payload.actions[0].model_dump(mode="json"), + ) + block_actions_service = factory.create_slack_block_actions_service() + await block_actions_service.handle_block_actions(payload) + + +@kafka_router.subscriber( + config.view_submission_topic, + group_id=f"{config.consumer_group_id}-view-submission", +) +async def handle_slack_view_submission( + payload: SquarebotSlackViewSubmissionValue, + context: Annotated[ConsumerContext, Depends(consumer_context_dependency)], +) -> None: + """Handle a Slack view submission interaction.""" + logger = context.logger + factory = context.factory + + logger.debug( + "Handling view submission", + payload=payload.model_dump(mode="json"), + ) + view_service = factory.create_slack_view_service() + await view_service.handle_view_submission(payload) diff --git a/src/templatebot/logging.py b/src/templatebot/logging.py deleted file mode 100644 index 885027d..0000000 --- a/src/templatebot/logging.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Context-local logger.""" - -__all__ = ["response_logger", "get_response_logger"] - -import contextvars - -response_logger = contextvars.ContextVar("response_logger") -"""A context-local structlog logger. - -This logger is set by templatebot.middleware.logging. - -See also --------- -get_response_logger - -Examples --------- -Usage: - ->>> logger = response_logger.get() ->>> logger.info(key='value') -""" - - -def get_response_logger(): - """Get the context-local structlog logger with bound request context. - - This logger is set by `templatebot.middleware.logging`. - - Examples - -------- - Usage: - - .. code-block:: python - - from templatebot.logging import get_response_logger - logger = get_response_logger() - logger.info('Some message', somekey='somevalue') - - An alternative way to get the logger is through the ``request`` instance - inside the handler. For example: - - .. code-block:: python - - @routes.get('/') - async def get_index(request): - logger = request['logger'] - logger.info('Logged message', somekey='somevalue') - """ - return response_logger.get() diff --git a/src/templatebot/main.py b/src/templatebot/main.py new file mode 100644 index 0000000..416d645 --- /dev/null +++ b/src/templatebot/main.py @@ -0,0 +1,66 @@ +"""The main application factory for the unfurlbot service. + +Notes +----- +Be aware that, following the normal pattern for FastAPI services, the app is +constructed when this module is loaded and is not deferred until a function is +called. +""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from importlib.metadata import metadata, version + +from fastapi import FastAPI +from safir.logging import configure_logging, configure_uvicorn_logging +from safir.middleware.x_forwarded import XForwardedMiddleware +from structlog import get_logger + +from .config import config +from .dependencies.consumercontext import consumer_context_dependency +from .handlers.internal import internal_router +from .handlers.kafka import kafka_router + +__all__ = ["app", "config"] + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """Set up and tear down the application.""" + # Any code here will be run when the application starts up. + logger = get_logger(__name__) + + await consumer_context_dependency.initialize() + + async with kafka_router.lifespan_context(app): + logger.info("Templatebot start up complete.") + yield + + # Any code here will be run when the application shuts down. + await consumer_context_dependency.aclose() + + +configure_logging( + profile=config.profile, + log_level=config.log_level, + name="templatebot", +) +configure_uvicorn_logging(config.log_level) + +app = FastAPI( + title="Templatebot", + description=metadata("templatebot")["Summary"], + version=version("templatebot"), + openapi_url=f"/{config.path_prefix}/openapi.json", + docs_url=f"/{config.path_prefix}/docs", + redoc_url=f"/{config.path_prefix}/redoc", + lifespan=lifespan, +) +"""The main FastAPI application for templatebot.""" + +# Attach the routers. +app.include_router(internal_router) +app.include_router(kafka_router) + +# Add middleware. +app.add_middleware(XForwardedMiddleware) diff --git a/src/templatebot/middleware/__init__.py b/src/templatebot/middleware/__init__.py deleted file mode 100644 index 07df344..0000000 --- a/src/templatebot/middleware/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""aiohttp.web middleware for the application.""" - -from .logging import bind_logger - -__all__ = ["setup_middleware"] - - -def setup_middleware(app): - """Add middleware to the application. - - Notes - ----- - This function includes the following middleware, in order: - - 1. `templatebot.middleware.bind_logger` - - Examples - -------- - Use it like this: - - .. code-block:: python - - app = web.Application() - setup_middleware(app) - """ - app.middlewares.append(bind_logger) diff --git a/src/templatebot/middleware/logging.py b/src/templatebot/middleware/logging.py deleted file mode 100644 index 34423dc..0000000 --- a/src/templatebot/middleware/logging.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Middleware that creates a response context-local structlog logger with -request information bound to it. -""" - -import uuid - -import structlog -from aiohttp import web - -from templatebot.logging import response_logger - - -@web.middleware -async def bind_logger(request, handler): - """Bind request metadata to the context-local structlog logger. - - This is an aiohttp.web middleware. - - Notes - ----- - This middleware initializes a new response-local structlog logger with - context bound to it. All logging calls within the context of a response - include this context. This makes it easy to search, filter, and aggregate - logs for a specififc request. For background, see - http://www.structlog.org/en/stable/getting-started.html#building-a-context - - The context fields are: - - ``request_id`` - A random UUID4 string that uniquely identifies the request. - ``path`` - The path of the request. - ``method`` - The http method of the request. - - Examples - -------- - **Setting up the middleware** - - Use the `templatebot.middleware.setup_middleware` function to set this up: - - .. code-block:: python - - app = web.Application() - setup_middleware(app) - - **Using the logger** - - Within a handler, you can access the logger directly from the 'logger' - key of the request object: - - .. code-block:: python - - @routes.get('/') - async def get_index(request): - logger = request['logger'] - logger.info('Logged message', somekey='somevalue') - - If the request object is not available, you can still get the logger - through the `templatebot.logging.get_response_logger` function: - - .. code-block:: python - - from templatebot.logging import get_response_logger - - logger = get_response_logger() - logger.info('My message', somekey='somevalue') - - Under the hood, you can also get this logger from the - `templatebot.logging.response_logger` context variable. For example: - - .. code-block:: python - - from templatebot.logging import response_logger - - logger = response_logger.get() - logger.info('My message', somekey='somevalue') - - The ``response_logger.get()`` syntax is because ``response_logger`` is a - `contextvars.ContextVar`. A `~contextvars.ContextVar` is isolated to each - asyncio Task, which makes it great for storing context specific to each - reponse. - - The ``request['logger']`` and `templatebot.logging.get_response_logger` - APIs are the best ways to get the logger. - - **Logger name** - - By default, the logger is named for the ``api.lsst.codes/loggerName`` - configuration field. If that configuration is not set, the logger name - falls back to ``__name__``. - """ - try: - logger_name = request.config_dict["api.lsst.codes/loggerName"] - except KeyError: - logger_name = __name__ - logger = structlog.get_logger(logger_name) - logger = logger.new( - request_id=str(uuid.uuid4()), - path=request.path, - method=request.method, - ) - - # Add the logger to the ContextVar - response_logger.set(logger) - - # Also add the logger to the request instance - request["logger"] = logger - - response = await handler(request) - - return response diff --git a/src/templatebot/roothandlers/__init__.py b/src/templatebot/roothandlers/__init__.py deleted file mode 100644 index ebaae57..0000000 --- a/src/templatebot/roothandlers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .index import get_index - -__all__ = ["get_index"] diff --git a/src/templatebot/roothandlers/index.py b/src/templatebot/roothandlers/index.py deleted file mode 100644 index 049908c..0000000 --- a/src/templatebot/roothandlers/index.py +++ /dev/null @@ -1,11 +0,0 @@ -from aiohttp import web - -from templatebot.routes import root_routes - -__all__ = ["get_index"] - - -@root_routes.get("/") -async def get_index(request): - name = request.config_dict["api.lsst.codes/name"] - return web.Response(text=name) diff --git a/src/templatebot/routes.py b/src/templatebot/routes.py deleted file mode 100644 index 970a340..0000000 --- a/src/templatebot/routes.py +++ /dev/null @@ -1,40 +0,0 @@ -"""API routes.""" - -from aiohttp import web - -__all__ = ["root_routes", "routes", "init_root_routes", "init_routes"] - -root_routes = web.RouteTableDef() -"""Routes for the root application that serves from '/' - -Application-specific routes don't get attached here. In practice, only routes -for metrics and health checks get attached to this table. Attach public APIs -to `routes` instead since those are accessible from the public API gateway and -are prefixed with the application name. -""" - -routes = web.RouteTableDef() -"""Routes for the public API that serves from '//'. -""" - - -def init_routes(): - """Initialize the route table and handlers from the application APIs, - served at ``//``. - """ - # Import handlers so that they are registered with the routes table via - # decorators. - import templatebot.handlers # noqa: F401 - - return routes - - -def init_root_routes(): - """Initialize the route table and handlers for the root APIs (not the - ones publicly available). - """ - # Import handlers so that they are registered with the routes table via - # decorators. - import templatebot.roothandlers # noqa: F401 - - return root_routes diff --git a/src/templatebot/services/__init__.py b/src/templatebot/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/templatebot/services/slackblockactions.py b/src/templatebot/services/slackblockactions.py new file mode 100644 index 0000000..f194e3e --- /dev/null +++ b/src/templatebot/services/slackblockactions.py @@ -0,0 +1,146 @@ +"""Slack service for handling block actions.""" + +from __future__ import annotations + +from rubin.squarebot.models.kafka import SquarebotSlackBlockActionsValue +from rubin.squarebot.models.slack import ( + SlackBlockActionBase, + SlackStaticSelectAction, +) +from structlog.stdlib import BoundLogger +from templatekit.repo import FileTemplate, ProjectTemplate + +from templatebot.config import config +from templatebot.constants import ( + SELECT_FILE_TEMPLATE_ACTION, + SELECT_PROJECT_TEMPLATE_ACTION, +) +from templatebot.services.template import TemplateService +from templatebot.storage.repo import RepoManager +from templatebot.storage.slack import SlackWebApiClient + +__all__ = ["SlackBlockActionsService"] + + +class SlackBlockActionsService: + """A service for processing Slack block actions.""" + + def __init__( + self, + logger: BoundLogger, + slack_client: SlackWebApiClient, + template_service: TemplateService, + repo_manager: RepoManager, + ) -> None: + self._logger = logger + self._slack_client = slack_client + self._template_service = template_service + self._repo_manager = repo_manager + + async def handle_block_actions( + self, payload: SquarebotSlackBlockActionsValue + ) -> None: + """Handle a Slack block_actions interaction.""" + for action in payload.actions: + if action.action_id == SELECT_PROJECT_TEMPLATE_ACTION: + await self.handle_project_template_selection( + action=action, payload=payload + ) + elif action.action_id == SELECT_FILE_TEMPLATE_ACTION: + await self.handle_file_template_selection( + action=action, payload=payload + ) + + async def handle_project_template_selection( + self, + *, + action: SlackBlockActionBase, + payload: SquarebotSlackBlockActionsValue, + ) -> None: + """Handle a project template selection.""" + if not isinstance(action, SlackStaticSelectAction): + raise TypeError( + f"Expected action for {SELECT_PROJECT_TEMPLATE_ACTION} to be " + f"a SlackStaticSelectAction, but got {type(action)}" + ) + selected_option = action.selected_option + self._logger.debug( + "Selected project template", + value=selected_option.value, + text=selected_option.text.text, + ) + + if not payload.channel: + raise ValueError("No channel in payload") + original_message_channel = payload.channel.id + if not payload.message: + raise ValueError("No message in payload") + original_message_ts = payload.message.ts + + git_ref = "main" + + template = self._repo_manager.get_repo(gitref=git_ref)[ + selected_option.value + ] + if not isinstance(template, ProjectTemplate): + raise TypeError( + f"Expected {selected_option.value} template to be a " + f"ProjectTemplate, but got {type(template)}" + ) + + await self._template_service.show_project_template_modal( + user_id=payload.user.id, + trigger_id=payload.trigger_id, + message_ts=original_message_ts, + channel_id=original_message_channel, + template=template, + git_ref=git_ref, + repo_url=str(config.template_repo_url), + ) + + async def handle_file_template_selection( + self, + *, + action: SlackBlockActionBase, + payload: SquarebotSlackBlockActionsValue, + ) -> None: + """Handle a file template selection.""" + if not isinstance(action, SlackStaticSelectAction): + raise TypeError( + f"Expected action for {SELECT_FILE_TEMPLATE_ACTION} to be " + f"a SlackStaticSelectAction, but got {type(action)}" + ) + selected_option = action.selected_option + self._logger.debug( + "Selected file template", + value=selected_option.value, + text=selected_option.text.text, + ) + + if not payload.channel: + raise ValueError("No channel in payload") + original_message_channel = payload.channel.id + if not payload.message: + raise ValueError("No message in payload") + original_message_ts = payload.message.ts + + git_ref = "main" + + template = self._repo_manager.get_repo(gitref=git_ref)[ + selected_option.value + ] + if not isinstance(template, FileTemplate): + raise TypeError( + f"Expected {selected_option.value} template to be a " + f"ProjectTemplate, but got {type(template)}" + ) + + await self._template_service.show_file_template_modal( + user_id=payload.user.id, + trigger_id=payload.trigger_id, + message_ts=original_message_ts, + channel_id=original_message_channel, + template=template, + git_ref=git_ref, + repo_url=str(config.template_repo_url), + ) diff --git a/src/templatebot/services/slackmessage.py b/src/templatebot/services/slackmessage.py new file mode 100644 index 0000000..e818823 --- /dev/null +++ b/src/templatebot/services/slackmessage.py @@ -0,0 +1,190 @@ +"""Service for processing Slack messages.""" + +from __future__ import annotations + +import re + +from rubin.squarebot.models.kafka import ( + SquarebotSlackAppMentionValue, + SquarebotSlackMessageValue, +) +from structlog.stdlib import BoundLogger + +from templatebot.storage.slack import ( + SlackChatPostMessageRequest, + SlackWebApiClient, +) +from templatebot.storage.slack.blockkit import ( + SlackContextBlock, + SlackMrkdwnTextObject, + SlackSectionBlock, +) + +from .templaterepo import TemplateRepoService + +MENTION_PATTERN = re.compile(r"<(@[a-zA-Z0-9]+|!subteam\^[a-zA-Z0-9]+)>") +"""Pattern for Slack mentions.""" + + +class SlackMessageService: + """A service for processing Slack messages.""" + + def __init__( + self, + logger: BoundLogger, + slack_client: SlackWebApiClient, + template_repo_service: TemplateRepoService, + ) -> None: + self._logger = logger + self._slack_client = slack_client + self._template_repo_service = template_repo_service + + async def handle_im_message( + self, message: SquarebotSlackMessageValue + ) -> None: + """Handle a Slack message. + + In the Squarebot ecosystem, all apps consume all messages and act on + a message if it contains content the app cares about. Templatebot + cares about the "create project" and "create file" commands. + """ + self._logger.debug( + "Slack message text", + text=message.text, + is_bot=message.is_bot, + username=message.user, + ) + self._logger.debug( + "Full Slack IM message", + body=message.model_dump(mode="json"), + ) + if message.is_bot: + self._logger.debug("Ignoring message from bot") + return + # Process the message + await self._handle_message_text(message.text, message) + + async def handle_app_mention( + self, message: SquarebotSlackAppMentionValue + ) -> None: + """Handle a Slack app mention.""" + self._logger.debug( + "Slack app mention text", + text=message.text, + ) + # Process the message + await self._handle_message_text(message.text, message) + + async def _handle_message_text( + self, + text: str, + original_message: SquarebotSlackMessageValue + | SquarebotSlackAppMentionValue, + ) -> None: + """Handle a message text.""" + self._logger.debug( + "Slack message text", + text=text, + ) + # Process the message + text = text.lower().strip() + if "create project" in text: + await self._handle_create_project(original_message) + elif "create file" in text: + await self._handle_create_file(original_message) + else: + # Strip out mentions + text = MENTION_PATTERN.sub("", original_message.text) + # normalize + text = " ".join(text.lower().split()) + + # determine if "help" is the only word + if text in ("help", "help!", "help?"): + await self._handle_help(original_message) + + async def _handle_create_project( + self, + message: SquarebotSlackMessageValue | SquarebotSlackAppMentionValue, + ) -> None: + """Handle a "create project" message.""" + self._logger.info("Creating a project") + thread_ts: str | None = None + if hasattr(message, "thread_ts") and message.thread_ts: + thread_ts = message.thread_ts + + if not message.user: + self._logger.warning( + "Project creation is being requested via a message that " + "doesn't have a user ID", + message=message.model_dump(mode="json"), + ) + return + await self._template_repo_service.handle_project_template_selection( + user_id=message.user, + channel_id=message.channel, + parent_ts=thread_ts if isinstance(thread_ts, str) else None, + ) + + async def _handle_create_file( + self, + message: SquarebotSlackMessageValue | SquarebotSlackAppMentionValue, + ) -> None: + """Handle a "create file" message.""" + self._logger.info("Creating a file") + thread_ts: str | None = None + if hasattr(message, "thread_ts") and message.thread_ts: + thread_ts = message.thread_ts + if not message.user: + self._logger.warning( + "File creation is being requested via a message that " + "doesn't have a user ID", + message=message.model_dump(mode="json"), + ) + return + await self._template_repo_service.handle_file_template_selection( + user_id=message.user, + channel_id=message.channel, + parent_ts=thread_ts if isinstance(thread_ts, str) else None, + ) + + async def _handle_help( + self, + message: SquarebotSlackMessageValue | SquarebotSlackAppMentionValue, + ) -> None: + """Handle a "help" message.""" + self._logger.info("Sending help message") + help_summary = ( + "Create a new GitHub repo from a template: `create project`.\\n" + "Create a snippet of file from a template: `create file`." + ) + section_bock = SlackSectionBlock( + text=SlackMrkdwnTextObject( + text=( + "• Create a GitHub repo from a template: " + "```create project```\n" + "• Create a file or snippet from a template: " + "```create file```" + ) + ) + ) + context_block = SlackContextBlock( + elements=[ + SlackMrkdwnTextObject( + text=( + "Handled by . The template repository is " + "https://github.com/lsst/templates." + ) + ) + ] + ) + thread_ts: str | None = None + if hasattr(message, "thread_ts") and message.thread_ts: + thread_ts = message.thread_ts + reply = SlackChatPostMessageRequest( + channel=message.channel, + thread_ts=thread_ts, + text=help_summary, + blocks=[section_bock, context_block], + ) + await self._slack_client.send_chat_post_message(reply) diff --git a/src/templatebot/services/slackview.py b/src/templatebot/services/slackview.py new file mode 100644 index 0000000..4c77b80 --- /dev/null +++ b/src/templatebot/services/slackview.py @@ -0,0 +1,107 @@ +"""A service for handling Slack view interactions.""" + +from __future__ import annotations + +from rubin.squarebot.models.kafka import SquarebotSlackViewSubmissionValue +from structlog.stdlib import BoundLogger + +from templatebot.constants import TEMPLATE_VARIABLES_MODAL_CALLBACK_ID +from templatebot.storage.repo import RepoManager +from templatebot.storage.slack import SlackWebApiClient +from templatebot.storage.slack.variablesmodal import ( + TemplateVariablesModalMetadata, +) + +from .template import TemplateService + + +class SlackViewService: + """A service for handling Slack view interactions.""" + + def __init__( + self, + logger: BoundLogger, + slack_client: SlackWebApiClient, + repo_manager: RepoManager, + template_service: TemplateService, + ) -> None: + self._logger = logger + self._slack_client = slack_client + self._repo_manager = repo_manager + self._template_service = template_service + + async def handle_view_submission( + self, payload: SquarebotSlackViewSubmissionValue + ) -> None: + """Handle a Slack view submission interaction. + + This is a glue layer that the Kafka consumer can call directly to + handle view submission events. This service then delegates to the + appropriate domain-specific service like TemplateService to handle + the submission. This serivce is also responsible for extracting + information from the view submission payload so that the domain + service can focus on the business logic. + """ + self._logger.debug( + "Got view submission", payload=payload.model_dump(mode="json") + ) + if payload.view["callback_id"] == TEMPLATE_VARIABLES_MODAL_CALLBACK_ID: + await self._handle_template_render(payload) + + async def _handle_template_render( + self, payload: SquarebotSlackViewSubmissionValue + ) -> None: + """Handle the submission of a template variables modal to create + either a new file or project. + + See `TemplateVariablesModal` for more information on the modal. + """ + if "private_metadata" not in payload.view: + self._logger.error( + "No private metadata in variables modal view submission", + payload=payload.model_dump(mode="json"), + ) + return + private_metadata = TemplateVariablesModalMetadata.model_validate_json( + payload.view["private_metadata"] + ) + templates_repo = self._repo_manager.get_repo(private_metadata.git_ref) + template = templates_repo[private_metadata.template_name] + + # Extract the variables from the submission. Note that the submission + # values aren't the same as the template variables because templatekit + # offers compuound variables like preset_groups and preset_options + # that effectively set multiple cookiecutter template variables + # based on a modal value. The Template service is responsible for + # translating these submission values into template variables. + modal_values: dict[str, str] = {} + for block_state in payload.view["state"]["values"].values(): + for action_id, action_state in block_state.items(): + if action_state["type"] == "plain_text_input": + modal_values[action_id] = action_state["value"] + elif action_state["type"] == "static_select": + modal_values[action_id] = action_state["selected_option"][ + "value" + ] + else: + self._logger.warning( + "Unhandled action type in view submission", + action_id=action_id, + action_type=action_state["type"], + action_state=action_state, + ) + + if private_metadata.type == "file": + await self._template_service.create_file_from_template( + template=template, + modal_values=modal_values, + trigger_message_ts=private_metadata.trigger_message_ts, + trigger_channel_id=private_metadata.trigger_channel_id, + ) + elif private_metadata.type == "project": + await self._template_service.create_project_from_template( + template=template, + modal_values=modal_values, + trigger_message_ts=private_metadata.trigger_message_ts, + trigger_channel_id=private_metadata.trigger_channel_id, + ) diff --git a/src/templatebot/services/template.py b/src/templatebot/services/template.py new file mode 100644 index 0000000..759aa3c --- /dev/null +++ b/src/templatebot/services/template.py @@ -0,0 +1,588 @@ +"""Template service.""" + +from __future__ import annotations + +import re +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + +from cookiecutter.main import cookiecutter +from httpx import AsyncClient +from structlog.stdlib import BoundLogger +from templatekit.repo import BaseTemplate, FileTemplate, ProjectTemplate + +from templatebot.storage.authordb import AuthorDb +from templatebot.storage.gitclone import GitClone +from templatebot.storage.githubappclientfactory import GitHubAppClientFactory +from templatebot.storage.githubrepo import GitHubRepo +from templatebot.storage.ltdclient import LtdClient +from templatebot.storage.slack import ( + SlackChatUpdateMessageRequest, + SlackWebApiClient, +) +from templatebot.storage.slack.blockkit import ( + SlackBlock, + SlackMrkdwnTextObject, + SlackSectionBlock, +) +from templatebot.storage.slack.variablesmodal import TemplateVariablesModal + +__all__ = ["TemplateService"] + + +class TemplateService: + """A service for operating with templates. + + Features include: + + - Having a user configure a template through a Slack modal view + - Rendering a template with user-provided values and running the + configuration of that repository and LSST the Docs services. + """ + + def __init__( + self, + *, + logger: BoundLogger, + http_client: AsyncClient, + slack_client: SlackWebApiClient, + github_client_factory: GitHubAppClientFactory, + ltd_client: LtdClient, + ) -> None: + self._logger = logger + self._http_client = http_client + self._slack_client = slack_client + self._github_client_factory = github_client_factory + self._ltd_client = ltd_client + + async def show_file_template_modal( + self, + *, + user_id: str, + trigger_id: str, + message_ts: str, + channel_id: str, + template: FileTemplate, + git_ref: str, + repo_url: str, + ) -> None: + """Show a modal for selecting a file template.""" + if len(template.config["dialog_fields"]) == 0: + await self._respond_with_nonconfigurable_content( + template=template, + channel_id=channel_id, + trigger_message_ts=message_ts, + ) + else: + await self._open_template_modal( + template=template, + trigger_id=trigger_id, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=message_ts, + trigger_channel_id=channel_id, + ) + + async def show_project_template_modal( + self, + *, + user_id: str, + trigger_id: str, + message_ts: str, + channel_id: str, + template: ProjectTemplate, + git_ref: str, + repo_url: str, + ) -> None: + """Show a modal for selecting a project template.""" + await self._open_template_modal( + template=template, + trigger_id=trigger_id, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=message_ts, + trigger_channel_id=channel_id, + ) + + async def _open_template_modal( + self, + *, + template: FileTemplate | ProjectTemplate, + trigger_id: str, + git_ref: str, + repo_url: str, + trigger_message_ts: str | None = None, + trigger_channel_id: str | None = None, + ) -> None: + """Open a modal for configuring a template.""" + modal_view = TemplateVariablesModal.create( + template=template, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + await self._slack_client.open_view( + trigger_id=trigger_id, view=modal_view + ) + + async def _respond_with_nonconfigurable_content( + self, + *, + template: FileTemplate, + channel_id: str, + trigger_message_ts: str, + ) -> None: + """Respond with non-configurable content.""" + # TODO(jonathansick): render the template and send it back to the user + await self._slack_client.update_message( + message_update_request=SlackChatUpdateMessageRequest( + channel=channel_id, + ts=trigger_message_ts, + text=( + f"The {template.name} template does not require " + "configuration." + ), + ) + ) + + async def create_project_from_template( # noqa: PLR0912 PLR0915 C901 + self, + *, + template: ProjectTemplate, + modal_values: dict[str, str], + trigger_message_ts: str | None, + trigger_channel_id: str | None, + ) -> None: + """Create a GitHub repository and set up a project from a template.""" + if trigger_channel_id and trigger_message_ts: + await self._slack_client.update_message( + message_update_request=SlackChatUpdateMessageRequest( + channel=trigger_channel_id, + ts=trigger_message_ts, + text="I'm creating your new project...", + ) + ) + + # Values for the repository creation. We'll set this when possible + # during the pre-processing steps. + github_owner: str | None = None + github_name: str | None = None + github_homepage_url: str | None = None + github_description: str | None = None + + # Preprocessing steps. First convert values from the Slack modal into + # cookiecutter template variables. This expands the templatekit + # preset_groups and preset_options into the full set of template + # variables. + template_values = self._transform_modal_values( + template=template, modal_values=modal_values + ) + + # Expand the author_id variable into full author information, if + # present. + await self._expand_author_id_variable(template_values) + + # Variables for LSST the Docs registration + ltd_slug: str | None = None + ltd_title: str | None = None + github_repo_url: str | None = None + + if template.name.startswith("technote_"): + # Handle preprocessing steps for technotes. These have + # automatically assigned repository/serial numbers + await self._assign_technote_repo_serial(template_values) + github_owner = template_values["github_org"] + github_name = ( + f"{template_values['series'].lower()}" + f"-{template_values['serial_number']}" + ) + github_homepage_url = f"https://{github_name}.lsst.io/" + github_description = template_values["title"] + + ltd_slug = github_name + ltd_title = template_values["title"] + + elif template.name == "latex_lsstdoc": + # Preprocessing steps for change control documents hosted in + # docushare. These have manually-assigned serial numbers. + # + # In the latex_lsstdoc template, the series and serial_number are + # determined from the handle, which the author enters. + # This logic attempts to match this metadata + # and extract it, if necessary and possible. + handle = template_values["handle"] + handle_match = re.match( + r"(?P[A-Z]+)-(?P[0-9]+)", handle + ) + if handle_match is None: + # If the handle does not match the expected pattern, then + # we cannot determine the series and serial number. + # TODO(jonathansick): send a Slack message to the user + raise ValueError( + f"Cannot determine series and serial number from handle: " + f"{handle}" + ) + # TODO(jonathansick): Verify the document handle is not for a + # technote or for a document that already exists. + template_values["series"] = handle_match["series"] + template_values["serial_number"] = handle_match["serial_number"] + github_owner = template_values["github_org"] + github_name = handle.lower() + github_homepage_url = f"https://{github_name}.lsst.io/" + github_description = template_values["title"] + + ltd_slug = github_name + ltd_title = template_values["title"] + + elif template.name == "test_report": + # In test_report templates the series and serial_number are + # manually assigned. + github_name = ( + f"{template_values['series'].lower()}-" + f"{template_values['serial_number']}" + ) + github_owner = template_values["github_org"] + github_homepage_url = f"https://{github_name}.lsst.io/" + github_description = template_values["title"] + + ltd_slug = github_name + ltd_title = template_values["title"] + + elif template.name == "stack_package": + github_name = template_values["package_name"] + github_owner = template_values["github_org"] + github_description = "A package in the LSST Science Pipelines." + + else: + # A generic repository template. By definition the "name" + # is treated as teh github repo name, and the "github_org" + # is the owner. + github_name = template_values["name"] + github_owner = template_values["github_org"] + if "summary" in template_values: + github_description = template_values["summary"] + elif "description" in template_values: + github_description = template_values["description"] + + # Create the repository on GitHub + f = self._github_client_factory + github_client = await f.create_installation_client_for_org( + github_owner + ) + github_repo = GitHubRepo( + owner=github_owner, + name=github_name, + github_client=github_client, + logger=self._logger, + ) + github_repo_info = await github_repo.create_repo( + homepage=github_homepage_url, + description=github_description, + ) + github_repo_url = github_repo_info["html_url"] + + if ( + ltd_slug is not None + and ltd_title is not None + and github_repo_url is not None + ): + ltd_info = await self._ltd_client.register_ltd_product( + slug=ltd_slug, + title=ltd_title, + github_repo=github_repo_url, + main_mode="lsst_doc" + if template.name in ("latex_lsstdoc", "test_report") + else "git_refs", + ) + self._logger.info( + "Registered project on LSST the Docs", ltd_info=ltd_info + ) + + if github_repo_url is None: + raise RuntimeError("No GitHub repository URL was created.") + + # Render the template + with TemporaryDirectory() as tmp_dir: + project_dir = self._render_template( + template=template, + template_values=template_values, + tmp_dir=Path(tmp_dir), + ) + git_repo = GitClone.init_repo( + path=project_dir, github_client=github_client + ) + git_repo.commit("Initial commit") + git_repo.push(remote_url=github_repo_url, branch="main") + + # Update the Slack message with the rendered project details + reply_blocks: list[SlackBlock] = [] + reply_blocks.append( + SlackSectionBlock( + text=SlackMrkdwnTextObject(text="Your new project is ready!") + ) + ) + if ltd_slug is not None: + reply_blocks.append( + SlackSectionBlock( + fields=[ + SlackMrkdwnTextObject(text="*Web page:*"), + SlackMrkdwnTextObject(text="*Repository:*"), + SlackMrkdwnTextObject( + text=github_homepage_url or "Unavailable" + ), + SlackMrkdwnTextObject( + text=github_repo_url or "Unavailable" + ), + ] + ) + ) + reply_blocks.append( + SlackSectionBlock( + text=SlackMrkdwnTextObject( + text=( + "_The homepage link may return a 404 error " + "until the site has been built._" + ) + ) + ) + ) + if template.name in ("technote_rst", "technote_md"): + reply_blocks.append( + SlackSectionBlock( + text=SlackMrkdwnTextObject( + text=( + "To learn how to write a Rubin technote " + "visit " + "https://documenteer.lsst.io/technotes/" + ) + ), + ) + ) + else: + reply_blocks.append( + SlackSectionBlock( + fields=[ + SlackMrkdwnTextObject(text="*Repository:*"), + SlackMrkdwnTextObject( + text=github_repo_url or "Unavailable" + ), + ] + ) + ) + + if trigger_channel_id and trigger_message_ts: + await self._slack_client.update_message( + message_update_request=SlackChatUpdateMessageRequest( + channel=trigger_channel_id, + ts=trigger_message_ts, + text="Your new project is ready!", + blocks=reply_blocks, + ) + ) + + async def create_file_from_template( + self, + *, + template: FileTemplate, + modal_values: dict[str, str], + trigger_message_ts: str | None, + trigger_channel_id: str | None, + ) -> None: + """Create a file from a template.""" + # TODO(jonathansick): implement this + if trigger_channel_id and trigger_message_ts: + await self._slack_client.update_message( + message_update_request=SlackChatUpdateMessageRequest( + channel=trigger_channel_id, + ts=trigger_message_ts, + text=( + f"Creating a file from the {template.name} template." + ), + ) + ) + + def _transform_modal_values( # noqa: C901 + self, *, template: BaseTemplate, modal_values: dict[str, str] + ) -> dict[str, str]: + """Transform modal values into template variables.""" + # TODO(jonathansick): Relocate this into either TemplateVariablesModal + # (if we parse submissions back into with a subclass providing state) + # or into the Template class. + + # Drop any null fields so that we get the defaults from cookiecutter. + data = {k: v for k, v in modal_values.items() if v is not None} + + for field in template.config["dialog_fields"]: + if "preset_groups" in field: + # Handle as a preset_groups select menu + selected_label = data[field["label"]] + for option_group in field["preset_groups"]: + for option in option_group["options"]: + if option["label"] == selected_label: + data.update(dict(option["presets"].items())) + del data[field["label"]] + + elif "preset_options" in field: + # Handle as a preset select menu + selected_value = data[field["label"]] + for option in field["preset_options"]: + if option["value"] == selected_value: + data.update(dict(option["presets"].items())) + del data[field["label"]] + + elif field["component"] == "select": + # Handle as a regular select menu + try: + selected_value = data[field["key"]] + except KeyError: + # If field not in data, then it was not set, use defaults + continue + + # Replace any truncated values from select fields + # with full values + for option in field["options"]: + if option["value"] == selected_value: + data[field["key"]] = option["template_value"] + continue + + return data + + async def _expand_author_id_variable( + self, template_values: dict[str, str] + ) -> None: + """Expand the author_id variable into full author information + from lsst-texmf's authordb.yaml. + """ + author_id = template_values.get("author_id") + if not author_id: + return + + authordb = await AuthorDb.download(self._http_client) + # TODO(jonathansick): handle missing author_id with Slack message + author_info = authordb.get_author(author_id) + + template_values["first_author_given"] = author_info.given_name + template_values["first_author_family"] = author_info.family_name + template_values["first_author_orcid"] = author_info.orcid + template_values["first_author_affil_name"] = ( + author_info.affiliation_name + ) + template_values["first_author_affil_internal_id"] = ( + author_info.affiliation_id + ) + template_values["first_author_affil_address"] = ( + author_info.affiliation_address + ) + + async def _assign_technote_repo_serial( + self, template_values: dict[str, str] + ) -> None: + """Assign a repository serial number for a technote and update the + template values. + + The following standard technote template values are updated: + + - ``serial_number`` + """ + org_name = template_values["github_org"] + series = template_values["series"].lower() + + series_pattern = re.compile(r"^" + series + r"-(?P\d+)$") + + # Get repository names from GitHub for this org + f = self._github_client_factory + ghclient = await f.create_installation_client_for_org(org_name) + repo_iter = ghclient.getiter( + "/orgs{/org}/repos", url_vars={"org": org_name} + ) + series_numbers = [] + async for repo_info in repo_iter: + name = repo_info["name"].lower() + m = series_pattern.match(name) + if m is None: + continue + series_numbers.append(int(m.group("number"))) + + self._logger.debug( + "Collected existing numbers for series, series_numbers", + series=series, + series_numbers=series_numbers, + ) + + new_number = self._propose_number([int(n) for n in series_numbers]) + serial_number = f"{new_number:03d}" + repo_name = f"{series.lower()}-{serial_number}" + + self._logger.info( + "Selected new technote repo name", name=repo_name, org=org_name + ) + + # Update the template values. This relies on all technotes having + # the same variables structure. + template_values["serial_number"] = serial_number + + def _propose_number(self, series_numbers: list[int]) -> int: + """Propose a technote number given the list of available document + numbers. + + This algorithm starts from 1, increments numbers by 1, and will fill in + any gaps in the numbering scheme. + """ + series_numbers.sort() + + n_documents = len(series_numbers) + + if n_documents == 0: + return 1 + + for i in range(n_documents): + serial_number = series_numbers[i] + + if i == 0 and serial_number > 1: + return 1 + + if i + 1 == n_documents: + # it might be the next-highest number + return series_numbers[i] + 1 + + # check if the next number is missing + if series_numbers[i + 1] != serial_number + 1: + return serial_number + 1 + + raise RuntimeError("propose_number should not be in this state.") + + def _render_template( + self, + *, + template: ProjectTemplate, + template_values: dict[str, Any], + tmp_dir: Path, + ) -> Path: + """Render a project template to a local directory. + + Returns + ------- + pathlib.Path + Path to the rendered project itself, which is a subdirectory of + tmp_dir. + """ + cookiecutter( + str(template.path), + output_dir=str(tmp_dir), + overwrite_if_exists=True, + no_input=True, + extra_context=template_values, + ) + self._logger.debug("Rendered cookiecutter project") + + # Find the rendered directory. The actual name is templated so its + # easier to just find it. + subdirs = [x for x in tmp_dir.iterdir() if x.is_dir()] + if len(subdirs) > 1: + self._logger.warning( + "Found an unexpected number of possible repo dirs", + dirs=subdirs, + ) + return subdirs[0] diff --git a/src/templatebot/services/templaterepo.py b/src/templatebot/services/templaterepo.py new file mode 100644 index 0000000..3d8734d --- /dev/null +++ b/src/templatebot/services/templaterepo.py @@ -0,0 +1,162 @@ +"""Service for operations with template repositories.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator +from typing import TypeVar + +from structlog.stdlib import BoundLogger +from templatekit.repo import BaseTemplate + +from templatebot.constants import ( + SELECT_FILE_TEMPLATE_ACTION, + SELECT_PROJECT_TEMPLATE_ACTION, +) +from templatebot.storage.repo import RepoManager +from templatebot.storage.slack import ( + SlackChatPostMessageRequest, + SlackWebApiClient, +) +from templatebot.storage.slack.blockkit import ( + SlackMrkdwnTextObject, + SlackOptionGroupObject, + SlackOptionObject, + SlackPlainTextObject, + SlackSectionBlock, + SlackStaticSelectElement, +) + +__all__ = ["TemplateRepoService"] + +T = TypeVar("T", bound=BaseTemplate) +"""A type variable for a templatekit template type.""" + + +class TemplateRepoService: + """A service for operations with template repositories. + + This service is reponsible letting users select a template. + """ + + def __init__( + self, + repo_manager: RepoManager, + logger: BoundLogger, + slack_client: SlackWebApiClient, + ) -> None: + self._repo_manager = repo_manager + self._logger = logger + self._slack_client = slack_client + + async def handle_file_template_selection( + self, + user_id: str, + channel_id: str, + parent_ts: str | None, + ) -> None: + """Handle a file template selection.""" + repo = self._repo_manager.get_repo(gitref="main") + + option_groups = await self._generate_menu_options( + repo.iter_file_templates + ) + select_element = SlackStaticSelectElement( + placeholder=SlackPlainTextObject(text="Select a template"), + option_groups=option_groups, + action_id=SELECT_FILE_TEMPLATE_ACTION, + ) + block = SlackSectionBlock( + text=SlackMrkdwnTextObject( + text=( + f"<@{user_id}> what type of file or snippet do you want " + "to make?" + ) + ), + accessory=select_element, + ) + message = SlackChatPostMessageRequest( + channel=channel_id, + text="Select a file template", + blocks=[block], + thread_ts=parent_ts, + ) + await self._slack_client.send_chat_post_message(message) + + async def handle_project_template_selection( + self, + user_id: str, + channel_id: str, + parent_ts: str | None, + ) -> None: + """Handle a project template selection.""" + repo = self._repo_manager.get_repo(gitref="main") + + option_groups = await self._generate_menu_options( + repo.iter_project_templates + ) + select_element = SlackStaticSelectElement( + placeholder=SlackPlainTextObject(text="Select a template"), + option_groups=option_groups, + action_id=SELECT_PROJECT_TEMPLATE_ACTION, + ) + block = SlackSectionBlock( + text=SlackMrkdwnTextObject( + text=( + f"<@{user_id}> what type of project do you want " + "to make?" + ) + ), + accessory=select_element, + ) + message = SlackChatPostMessageRequest( + channel=channel_id, + text="Select a project template", + blocks=[block], + thread_ts=parent_ts, + ) + await self._slack_client.send_chat_post_message(message) + + async def _generate_menu_options( + self, + template_iterator: Callable[[], Iterator[T]], + ) -> list[SlackOptionGroupObject]: + """Generate a menu of template options.""" + # Group the templates + grouped_templates: dict[str, list[T]] = {} + for template in template_iterator(): + group = template.config["group"] + if group in grouped_templates: + grouped_templates[group].append(template) + else: + grouped_templates[group] = [template] + + # Sort the groups by name label + group_names = sorted(grouped_templates.keys()) + # Always put 'General' at the beginning + if "General" in group_names: + group_names.insert( + 0, group_names.pop(group_names.index("General")) + ) + + # Sort templates by label within each group + for group in grouped_templates.values(): + group.sort(key=lambda x: x.config["name"]) + + # Convert into a list of SlackOptionGroupObject + option_groups: list[SlackOptionGroupObject] = [] + for group_name in group_names: + group = grouped_templates[group_name] + options = [] + for template in group: + option = SlackOptionObject( + text=SlackPlainTextObject(text=template.config["name"]), + value=template.name, + ) + options.append(option) + option_group = SlackOptionGroupObject( + label=SlackPlainTextObject(text=group_name), + options=options, + ) + option_groups.append(option_group) + + return option_groups diff --git a/src/templatebot/storage/__init__.py b/src/templatebot/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/templatebot/storage/authordb.py b/src/templatebot/storage/authordb.py new file mode 100644 index 0000000..e1c056d --- /dev/null +++ b/src/templatebot/storage/authordb.py @@ -0,0 +1,153 @@ +"""Storage interface for lsst-texmf's authordb.yaml file.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +import yaml +from httpx import AsyncClient +from pydantic import BaseModel, Field, RootModel +from pylatexenc.latex2text import LatexNodes2Text + + +class AuthorDbAuthor(BaseModel): + """Model for an author entry in author.yaml file.""" + + name: str = Field(description="Author's family name") + + initials: str = Field(description="Author's given name") + + affil: list[str] = Field(default_factory=list, description="Affiliations") + + orcid: str | None = Field( + default=None, + description="Author's ORCiD identifier (optional)", + ) + + +class AuthorDbAuthors(RootModel): + """Model for the authors mapping in authordb.yaml file.""" + + root: dict[str, AuthorDbAuthor] + + def __getitem__(self, author_id: str) -> AuthorDbAuthor: + """Get an author entry by ID.""" + return self.root[author_id] + + +class AuthorDbYaml(BaseModel): + """Model for the authordb.yaml file in lsst/lsst-texmf.""" + + affiliations: dict[str, str] = Field( + description=( + "Mapping of affiliation IDs to affiliation names. Affiliations " + "are their name, a comma, and thier address." + ) + ) + + authors: AuthorDbAuthors = Field( + description="Mapping of author IDs to author information" + ) + + +@dataclass +class AuthorInfo: + """Consolidated author information.""" + + author_id: str + given_name: str + family_name: str + orcid: str + affiliation_name: str + affiliation_id: str + affiliation_address: str + + @classmethod + def create_from_db( + cls, + author_id: str, + db_author: AuthorDbAuthor, + db_affils: dict[str, str], + ) -> AuthorInfo: + """Create an AuthorInfo from an AuthorDbAuthor and affiliations.""" + # Transform orcid path to a full orcid.org URL + if db_author.orcid: + if db_author.orcid.startswith("http"): + orcid = db_author.orcid + else: + orcid = f"https://orcid.org/{db_author.orcid}" + else: + orcid = "" + + # Transform the first affiliation + if db_author.affil: + affiliation_id = db_author.affil[0] + affil = db_affils[affiliation_id] + parts = affil.split(",") + affiliation_name = parts[0] + if len(parts) > 1: + address_parts = [p.strip() for p in parts[1:]] + affiliation_address = ", ".join(address_parts) + else: + affiliation_address = "" + else: + affiliation_id = "" + affiliation_name = "" + affiliation_address = "" + + return cls( + author_id=author_id, + given_name=Latex(db_author.initials).to_text(), + family_name=Latex(db_author.name).to_text(), + orcid=orcid, + affiliation_name=Latex(affiliation_name).to_text(), + affiliation_id=affiliation_id, + affiliation_address=Latex(affiliation_address).to_text(), + ) + + +class AuthorDb: + """An interface for the lsst/lsst-texmf authordb.yaml file content.""" + + def __init__(self, data: AuthorDbYaml) -> None: + """Initialize the interface.""" + self._data = data + + @classmethod + def from_yaml(cls, yaml_data: str) -> AuthorDb: + """Create an AuthorDb from a string of YAML data.""" + return cls(AuthorDbYaml.model_validate(yaml.safe_load(yaml_data))) + + @classmethod + async def download(cls, http_client: AsyncClient) -> AuthorDb: + """Download a authordb.yaml from GitHub.""" + url = ( + "https://raw.githubusercontent.com/lsst/lsst-texmf" + "/main/etc/authordb.yaml" + ) + r = await http_client.get(url) + r.raise_for_status() + + return cls.from_yaml(r.text) + + def get_author(self, author_id: str) -> AuthorInfo: + """Get an author entry by ID.""" + db_author = self._data.authors[author_id] + db_affiliations = { + k: self._data.affiliations[k] for k in db_author.affil + } + return AuthorInfo.create_from_db(author_id, db_author, db_affiliations) + + +class Latex: + """A class for handling LaTeX text content.""" + + def __init__(self, tex: str) -> None: + self.tex = tex + + def to_text(self) -> str: + """Convert LaTeX to text.""" + text = LatexNodes2Text().latex_to_text(self.tex.strip()) + # Remove running spaces inside the content + return re.sub(" +", " ", text) diff --git a/src/templatebot/storage/gitclone.py b/src/templatebot/storage/gitclone.py new file mode 100644 index 0000000..a8028c0 --- /dev/null +++ b/src/templatebot/storage/gitclone.py @@ -0,0 +1,90 @@ +"""Storage interface to a local Git clone of a project repository.""" + +from __future__ import annotations + +import urllib.parse +from pathlib import Path +from typing import Self + +import git +from gidgethub.httpx import GitHubAPI + +from templatebot.config import config + +__all__ = ["GitClone"] + + +class GitClone: + """Storage interface to a local Git clone of a project repository.""" + + def __init__(self, *, repo: git.Repo, github_client: GitHubAPI) -> None: + self._repo = repo + self._github_client = github_client + + @classmethod + def init_repo( + cls, + *, + path: Path, + github_client: GitHubAPI, + default_branch: str = "main", + ) -> Self: + """Initialize a new Git repository in the given path and stage all + files. + """ + repo = git.Repo.init(str(path), b=default_branch) + repo.index.add(repo.untracked_files) + return cls(repo=repo, github_client=github_client) + + @property + def path(self) -> Path: + """Path to the local Git clone.""" + return Path(self._repo.working_dir) + + @property + def repo(self) -> git.Repo: + """The GitPython Repo object.""" + return self._repo + + def _create_authed_url(self, url: str) -> str: + repo_url_parts = urllib.parse.urlparse(url) + host = repo_url_parts.netloc.rsplit("@", 1)[-1] + if not self._github_client.oauth_token: + # Mostly for mypy's benefit. + raise RuntimeError("No OAuth token for installation client") + token = self._github_client.oauth_token + return repo_url_parts._replace( + scheme="https", netloc=f"squarebot:{token}@{host}" + ).geturl() + + def add_remote(self, url: str, *, name: str = "origin") -> git.Remote: + """Add a remote to the repository.""" + authed_url = self._create_authed_url(url) + return self._repo.create_remote(name, authed_url) + + def create_bot_actor(self) -> git.Actor: + """Create an actor for the bot.""" + email = ( + f"{config.github_app_id}+{config.github_username}" + "[bot]@users.noreply.github.com" + ) + return git.Actor(config.github_username, email) + + def commit(self, message: str) -> None: + """Commit all changes in the repository.""" + actor = self.create_bot_actor() + self._repo.index.commit(message, author=actor, committer=actor) + + def push(self, *, remote_url: str, branch: str = "main") -> None: + """Push the repository to the remote.""" + authed_url = self._create_authed_url(remote_url) + remote = self._repo.create_remote("tmp-squarebot", authed_url) + + try: + push_info = remote.push(f"{branch}:{branch}", force=True) + for result in push_info: + if result.flags & git.PushInfo.ERROR: + msg = f"Pushing {branch} failed: {result.summary}" + raise RuntimeError(msg) + finally: + git.Remote.remove(self.repo, "tmp-squarebot") diff --git a/src/templatebot/storage/githubappclientfactory.py b/src/templatebot/storage/githubappclientfactory.py new file mode 100644 index 0000000..371b05e --- /dev/null +++ b/src/templatebot/storage/githubappclientfactory.py @@ -0,0 +1,47 @@ +"""GitHub App client factory.""" + +from __future__ import annotations + +from gidgethub.httpx import GitHubAPI +from safir.github import GitHubAppClientFactory as SafirGitHubAppClientFactory + +__all__ = ["GitHubAppClientFactory"] + +# TODO(jonathansick): Upstream this to Safir + + +class GitHubAppClientFactory(SafirGitHubAppClientFactory): + """GitHub App client factory that can get an installation in an org.""" + + async def create_installation_client_for_org( + self, + owner: str, + ) -> GitHubAPI: + """Create a client authenticated as an installation of the GitHub App + for an organization. + + Parameters + ---------- + owner + The organization name. + + Returns + ------- + gidgethub.httpx.GitHubAPI + The installation client. + """ + app_jwt = self.get_app_jwt() + anon_client = self.create_anonymous_client() + + installation_url = "/app/installations" + async for installation in anon_client.getiter( + installation_url, jwt=app_jwt, iterable_key=None + ): + if installation["target_type"] == "Organization": + if installation["account"]["login"] == owner: + installation_id = installation["id"] + return await self.create_installation_client( + installation_id + ) + + raise ValueError(f"No installation found for {owner}") diff --git a/src/templatebot/storage/githubrepo.py b/src/templatebot/storage/githubrepo.py new file mode 100644 index 0000000..b54cb1e --- /dev/null +++ b/src/templatebot/storage/githubrepo.py @@ -0,0 +1,62 @@ +"""Storage interface to a GitHub repository.""" + +from __future__ import annotations + +from typing import Any + +from gidgethub.httpx import GitHubAPI +from structlog.stdlib import BoundLogger + +__all__ = ["GitHubRepo"] + + +class GitHubRepo: + """Storage interface to a GitHub repository.""" + + def __init__( + self, + *, + owner: str, + name: str, + github_client: GitHubAPI, + logger: BoundLogger, + ) -> None: + self._owner = owner + self._name = name + self._github_client = github_client + self._logger = logger + + async def create_repo( + self, + *, + homepage: str | None = None, + description: str | None = None, + allow_squash_merge: bool = False, + allow_merge_commit: bool = True, + allow_rebase_merge: bool = False, + delete_branch_on_merge: bool = True, + ) -> dict[str, Any]: + """Create the GitHub repository.""" + # Construct arguments to GitHub + data = { + "name": self._name, + # We want an empty repo for the render step. + "auto_init": False, + # Defaults for LSST + "has_projects": False, + "has_wiki": False, + "allow_squash_merge": allow_squash_merge, + "allow_merge_commit": allow_merge_commit, + "allow_rebase_merge": allow_rebase_merge, + "delete_branch_on_merge": delete_branch_on_merge, + } + if homepage is not None: + data["homepage"] = homepage + if description is not None: + data["description"] = description + self._logger.info("Creating repo", request_data=data) + return await self._github_client.post( + "/orgs{/org_name}/repos", + url_vars={"org_name": self._owner}, + data=data, + ) diff --git a/src/templatebot/storage/ltdclient.py b/src/templatebot/storage/ltdclient.py new file mode 100644 index 0000000..a0cb001 --- /dev/null +++ b/src/templatebot/storage/ltdclient.py @@ -0,0 +1,99 @@ +"""LSST the Docs admin API client.""" + +from __future__ import annotations + +from typing import Any + +from httpx import AsyncClient, BasicAuth +from pydantic import SecretStr +from structlog.stdlib import BoundLogger + +__all = ["LtdClient"] + + +class LtdClient: + """A client for interacting with the LSST the Docs admin API.""" + + def __init__( + self, + *, + username: str, + password: SecretStr, + http_client: AsyncClient, + logger: BoundLogger, + ) -> None: + self._username = username + self._password = password + self._http_client = http_client + self._logger = logger + + async def get_token(self) -> str: + """Get an auth token for LSST the Docs. + + Returns + ------- + token : `str` + The auth token (use in the 'username' field of basic auth, without + a separate password). + """ + url = "https://keeper.lsst.codes/token" + auth = BasicAuth( + username=self._username, password=self._password.get_secret_value() + ) + response = await self._http_client.get(url, auth=auth, timeout=10) + response.raise_for_status() + data = response.json() + return data["token"] + + async def register_ltd_product( + self, + *, + slug: str, + title: str, + github_repo: str, + main_mode: str = "git_refs", + ) -> dict[str, Any]: + """Register a new product on LSST the Docs. + + Parameters + ---------- + slug + The *slug* is the sub-domain component of the lsst.io domain. + title + The product's title. + github_repo + The URL of the product's source repository. + main_mode + The tracking mode of the main edition. See + https://ltd-keeper.lsst.io/editions.html#tracking-modes + + Returns + ------- + dict + The product resource, see + https://ltd-keeper.lsst.io/products.html#get--products-(slug) + """ + url = "https://keeper.lsst.codes/products/" + data = { + "title": title, + "slug": slug, + "doc_repo": github_repo, + "main_mode": main_mode, + "bucket_name": "lsst-the-docs", + "root_domain": "lsst.io", + "root_fastly_domain": "n.global-ssl.fastly.net", + } + + self._logger.debug("Registering product on LTD", url=url, payload=data) + + token = await self.get_token() + auth = BasicAuth(username=token, password="") + + r = await self._http_client.post(url, json=data, auth=auth) + r.raise_for_status() + product_url = r.headers["Location"] + + # Get data about the product + r = await self._http_client.get(product_url, auth=auth) + r.raise_for_status() + return r.json() diff --git a/src/templatebot/repo.py b/src/templatebot/storage/repo.py similarity index 82% rename from src/templatebot/repo.py rename to src/templatebot/storage/repo.py index c588b62..f664751 100644 --- a/src/templatebot/repo.py +++ b/src/templatebot/storage/repo.py @@ -1,9 +1,13 @@ """Management of the template repository.""" +from __future__ import annotations + import shutil import uuid +from pathlib import Path import git +from structlog.stdlib import BoundLogger from templatekit.repo import Repo @@ -13,34 +17,39 @@ class RepoManager: Parameters ---------- - url : `str` + url URL of a templatekit-compatible Git repository. - cache_dir : `pathlib.Path` + cache_dir Directory containing the cloned template repositories for all Git SHAs. logger ``structlog`` logger instance. """ - def __init__(self, *, url, cache_dir, logger): + def __init__( + self, *, url: str, cache_dir: Path, logger: BoundLogger + ) -> None: self._logger = logger self._url = url self._cache_dir = cache_dir - self._cache_dir.mkdir(exist_ok=True) + self._cache_dir.mkdir(exist_ok=True, parents=True) + + # keys are SHAs, values are repo paths + self._clones: dict[str, Path] = {} - self._clones = {} # keys are SHAs, values are Paths to the clone - self._clone_refs = {} # map branches/tags to SHAs + # map branches/tags to SHAs + self._clone_refs: dict[str, str] = {} - def clone(self, gitref="main"): + def clone(self, gitref: str = "main") -> Path: """Clone the template repository corresponding to Git ref. Parameters ---------- - gitref : `str` - A git ref (branch, tag, or SHA string) of the template repsitory. + gitref + A git ref (branch, tag, or SHA string) of the template repository. Returns ------- - path : `pathlib.Path` + pathlib.Path Path of the template repository clone. """ # Make a unique directory for this clone @@ -71,19 +80,19 @@ def clone(self, gitref="main"): return self._clones[head_sha] - def get_checkout_path(self, gitref): + def get_checkout_path(self, gitref: str) -> Path: """Get the path to a cloned repository for a given Git ref. If a clone is available, the method makes a new clone. Parameters ---------- - gitref : `str` + gitref A git ref (branch, tag, or SHA string) of the template repsitory. Returns ------- - path : `pathlib.Path` + pathlib.Path Path of the template repository clone. """ if gitref in self._clones: @@ -98,23 +107,23 @@ def get_checkout_path(self, gitref): # No record of this gitref; need to make a new clone return self.clone(gitref=gitref) - def get_repo(self, gitref): + def get_repo(self, gitref: str) -> Repo: """Open a repo clone with templatekit. Parameters ---------- - gitref : `str` + gitref A git ref (branch, tag, or SHA string) of the template repository. Returns ------- - repo : `templatekit.repo.Repo` + templatekit.repo.Repo Template repository. """ path = self.get_checkout_path(gitref=gitref) return Repo(path) - def delete_all(self): + def delete_all(self) -> None: """Delete all cloned repositories from the filesystem.""" self._logger.info("Deleting clones", dirname=self._cache_dir) shutil.rmtree(str(self._cache_dir)) @@ -122,8 +131,8 @@ def delete_all(self): self._clones = {} self._clone_refs = {} - def _refresh_checkout(self, gitref): - """Checks if the Git origin has a new SHA associated with its head, + def _refresh_checkout(self, gitref: str) -> None: + """Check if the Git origin has a new SHA associated with its head, and if so, creates a new clone with that SHA. """ existing_sha = self._clone_refs[gitref] diff --git a/src/templatebot/storage/slack/__init__.py b/src/templatebot/storage/slack/__init__.py new file mode 100644 index 0000000..609d3f4 --- /dev/null +++ b/src/templatebot/storage/slack/__init__.py @@ -0,0 +1,10 @@ +"""Slack API client and models.""" + +from ._client import SlackWebApiClient +from ._models import SlackChatPostMessageRequest, SlackChatUpdateMessageRequest + +__all__ = [ + "SlackWebApiClient", + "SlackChatPostMessageRequest", + "SlackChatUpdateMessageRequest", +] diff --git a/src/templatebot/storage/slack/_client.py b/src/templatebot/storage/slack/_client.py new file mode 100644 index 0000000..0b2df56 --- /dev/null +++ b/src/templatebot/storage/slack/_client.py @@ -0,0 +1,159 @@ +"""Slack client.""" + +from __future__ import annotations + +from typing import Any + +from httpx import AsyncClient +from pydantic import SecretStr +from structlog.stdlib import BoundLogger + +from ._models import SlackChatPostMessageRequest, SlackChatUpdateMessageRequest +from .views import SlackModalView + + +class SlackWebApiClient: + """A Slack client for the Web API. + + See https://api.slack.com/web for more information. + """ + + def __init__( + self, http_client: AsyncClient, token: SecretStr, logger: BoundLogger + ) -> None: + self._http_client = http_client + self._client_token = token + self._logger = logger + + async def send_chat_post_message( + self, message_request: SlackChatPostMessageRequest + ) -> dict: + """Send a chat.postMessage request to the Slack web API. + + Parameters + ---------- + body + The Slack chat.postMessage request body. + + Returns + ------- + dict + The JSON-decoded response from the Slack API as a dictionary. + + Raises + ------ + httpx.HTTPStatusError + Raised if the request fails. + """ + return await self.post_json( + method="chat.postMessage", + body=message_request.model_dump(mode="json", exclude_none=True), + ) + + async def update_message( + self, message_update_request: SlackChatUpdateMessageRequest + ) -> dict: + return await self.post_json( + method="chat.update", + body=message_update_request.model_dump( + mode="json", exclude_none=True + ), + ) + + async def open_view( + self, *, trigger_id: str, view: SlackModalView + ) -> dict: + """Open a view.""" + return await self.post_json( + method="views.open", + body={ + "trigger_id": trigger_id, + "view": view.model_dump(mode="json", exclude_none=True), + }, + ) + + async def post_json(self, *, method: str, body: dict[str, Any]) -> dict: + """Send JSON-encoded POST request to the Slack web API. + + Parameters + ---------- + method + The Slack API method to call. See + https://api.slack.com/web#methods for a list of methods. + body + A JSON-encodable dictionary to send as the request body. + + Returns + ------- + dict + The JSON-decoded response from the Slack API as a dictionary. + + Raises + ------ + httpx.HTTPStatusError + Raised if the request fails. + """ + url = self._format_url(method) + r = await self._http_client.post( + url, + json=body, + headers=self.create_headers(), + ) + r.raise_for_status() + resp_json = r.json() + if not resp_json["ok"]: + self._logger.error( + "Failed to send Slack message", + response=resp_json, + status_code=r.status_code, + message=body, + ) + return resp_json + + async def get( + self, *, method: str, params: dict[str, Any] | None = None + ) -> dict: + """Send a GET request to the Slack web API. + + Parameters + ---------- + method + The Slack API method to call. See + https://api.slack.com/web#methods for a list of methods. + params + A dictionary of query parameters to send with the request. + + Returns + ------- + dict + The JSON-decoded response from the Slack API as a dictionary. + + Raises + ------ + httpx.HTTPStatusError + Raised if the request fails. + """ + url = self._format_url(method) + + r = await self._http_client.get( + url, + params=params, + headers=self.create_headers(), + ) + r.raise_for_status() + return r.json() + + def _format_url(self, method: str) -> str: + """Format a URL for a Slack Web API endpoint.""" + if not method.startswith("/"): + method = method.lstrip("/") + return f"https://slack.com/api/{method}" + + def create_headers(self) -> dict[str, str]: + """Create headers for Slack API requests.""" + return { + "content-type": "application/json; charset=utf-8", + "authorization": ( + f"Bearer {self._client_token.get_secret_value()}" + ), + } diff --git a/src/templatebot/storage/slack/_models.py b/src/templatebot/storage/slack/_models.py new file mode 100644 index 0000000..d5185ec --- /dev/null +++ b/src/templatebot/storage/slack/_models.py @@ -0,0 +1,83 @@ +"""Slack Web API models.""" + +from __future__ import annotations + +from typing import Self + +from pydantic import BaseModel, Field, model_validator + +from .blockkit import SlackBlock + +__all__ = [ + "SlackChatPostMessageRequest", + "SlackChatUpdateMessageRequest", +] + + +class SlackChatPostMessageRequest(BaseModel): + """A request body for the Slack ``chat.postMessage`` method. + + See https://api.slack.com/methods/chat.postMessage for more information. + """ + + channel: str = Field( + ..., description="The channel ID.", examples=["C1234567890"] + ) + + thread_ts: str | None = Field( + None, + description="The timestamp of the parent message in a thread.", + ) + + reply_broadcast: bool | None = Field( + None, + description=( + "Whether to broadcast the message to the channel from a thread " + "(see ``thread_ts``)." + ), + ) + + text: str | None = Field( + None, description="The text of the message as a fallback for blocks." + ) + + mrkdwn: bool = Field( + True, description="Whether the text is formatted using Slack markdown." + ) + + blocks: list[SlackBlock] | None = Field( + None, description="The blocks that make up the message." + ) + + @model_validator(mode="after") + def validate_text_fallback(self) -> Self: + """Ensure that the text field is provided if blocks are not.""" + if not self.text and not self.blocks: + raise ValueError("Either `text` or `blocks` must be provided.") + return self + + +class SlackChatUpdateMessageRequest(BaseModel): + """A request body for the Slack ``chat.update`` method. + + See https://api.slack.com/methods/chat.update for more information. + """ + + channel: str = Field( + ..., description="The channel ID.", examples=["C1234567890"] + ) + + ts: str = Field(..., description="The timestamp of the message to update.") + + text: str | None = Field(None, description="The new text of the message.") + + blocks: list[SlackBlock] | None = Field( + None, description="The new blocks that make up the message." + ) + + @model_validator(mode="after") + def validate_text_or_blocks(self) -> Self: + """Ensure that either text or blocks are provided.""" + if not self.text and not self.blocks: + raise ValueError("Either `text` or `blocks` must be provided.") + return self diff --git a/src/templatebot/storage/slack/blockkit.py b/src/templatebot/storage/slack/blockkit.py new file mode 100644 index 0000000..bb5ecc5 --- /dev/null +++ b/src/templatebot/storage/slack/blockkit.py @@ -0,0 +1,554 @@ +"""Slack Block Kit models.""" + +from __future__ import annotations + +from abc import ABC +from typing import Annotated, Literal, Self + +from pydantic import BaseModel, Field, model_validator + +__all__ = [ + "SlackBlock", + "SlackConfirmationDialogObject", + "SlackContextBlock", + "SlackInputBlock", + "SlackMrkdwnTextObject", + "SlackOptionGroupObject", + "SlackOptionObject", + "SlackPlainTextInputElement", + "SlackPlainTextObject", + "SlackSectionBlock", + "SlackSectionBlockAccessoryTypes", + "SlackStaticSelectElement", + "SlackTextObjectBase", + "SlackTextObjectType", +] + +block_id_field: str | None = Field( + None, + description="A unique identifier for the block.", + max_length=255, +) + + +class SlackSectionBlock(BaseModel): + """A Slack section Block Kit block. + + Reference: https://api.slack.com/reference/block-kit/blocks#section + """ + + type: Literal["section"] = Field( + "section", + description=( + "The type of block. Reference: " + "https://api.slack.com/reference/block-kit/blocks" + ), + ) + + block_id: Annotated[str | None, block_id_field] = None + + text: SlackTextObjectType | None = Field( + None, + description=( + "The text to display in the block. Not required if `fields` is " + "provided." + ), + ) + + # Fields can take other types of elements. + fields: list[SlackTextObjectType] | None = Field( + None, + description=( + "An array of text objects. Each element of the array is a " + "text object, and is rendered as a separate paragraph." + ), + min_length=1, + max_length=10, + ) + + accessory: SlackSectionBlockAccessoryTypes | None = Field( + None, + description=( + "An accessory is an interactive element that can be displayed " + "within a section block. For example, a button, select menu, " + "or datepicker." + ), + ) + + @model_validator(mode="after") + def validate_text_or_fields(self) -> Self: + """Ensure that either `text` or `fields` is provided.""" + if not self.text and not self.fields: + raise ValueError("Either `text` or `fields` must be provided.") + return self + + @model_validator(mode="after") + def validate_fields_length(self) -> Self: + """Ensure that the max length of all text objects is not more than + 2000 characters. + """ + for i, field in enumerate(self.fields or []): + if len(field.text) > 2000: + raise ValueError( + f"The length of a field text element {i} must be <= 2000." + ) + return self + + @model_validator(mode="after") + def validate_text_length(self) -> Self: + """Ensure that the text length is not more than 3000 characters.""" + if self.text and len(self.text.text) > 3000: + raise ValueError("The length of the text must be <= 3000.") + return self + + +class SlackContextBlock(BaseModel): + """A Slack block for displaying contextual info. + + Reference: https://api.slack.com/reference/block-kit/blocks#context + """ + + type: Literal["context"] = Field( + "context", + description=( + "The type of block. Reference: " + "https://api.slack.com/reference/block-kit/blocks" + ), + ) + + block_id: Annotated[str | None, block_id_field] = None + + # image elements can also be supported when available + elements: list[SlackTextObjectType] = Field( + ..., + description=( + "An array of text objects. Each element of the array is a " + "text or image object, and is rendered in a separate context line." + "Maximum of 10 elements." + ), + min_length=1, + max_length=10, + ) + + +class SlackInputBlock(BaseModel): + """A Slack input block for collecting user input. + + Reference: https://api.slack.com/reference/block-kit/blocks#input + """ + + type: Literal["input"] = Field( + "input", + description=( + "The type of block. Reference: " + "https://api.slack.com/reference/block-kit/blocks" + ), + ) + + block_id: Annotated[str | None, block_id_field] = None + + label: SlackPlainTextObject = Field( + ..., + description=( + "A label that appears above an input element. " + "Maximum length of 2000 characters." + ), + max_length=2000, + ) + + element: SlackStaticSelectElement | SlackPlainTextInputElement = Field( + ..., description="An input element." + ) + + dispatch_action: bool = Field( + False, + description=( + "A boolean value that indicates whether the input element " + "should dispatch action payloads." + ), + ) + + hint: SlackPlainTextObject | None = Field( + None, + description=( + "A plain text object that defines a plain text element that " + "apppears below an input element in a lighter font. " + "Maximum length of 2000 characters." + ), + max_length=2000, + ) + + optional: bool = Field( + False, + description=( + "A boolean value that indicates whether the input element may be " + "empty when a user submits the modal." + ), + ) + + +class SlackTextObjectBase(BaseModel, ABC): + """A base class for Slack Block Kit text objects.""" + + type: Literal["plain_text", "mrkdwn"] = Field( + ..., description="The type of object." + ) + + text: str = Field(..., description="The text to display.") + + def __len__(self) -> int: + """Return the length of the text.""" + return len(self.text) + + +class SlackPlainTextObject(SlackTextObjectBase): + """A plain_text composition object. + + https://api.slack.com/reference/block-kit/composition-objects#text + """ + + type: Literal["plain_text"] = Field( + "plain_text", description="The type of object." + ) + + emoji: bool = Field( + True, + description=( + "Indicates whether emojis in text should be escaped into colon " + "emoji format." + ), + ) + + +class SlackMrkdwnTextObject(SlackTextObjectBase): + """A mrkdwn text composition object. + + https://api.slack.com/reference/block-kit/composition-objects#text + """ + + type: Literal["mrkdwn"] = Field( + "mrkdwn", description="The type of object." + ) + + verbatim: bool = Field( + False, + description=( + "Indicates whether the text should be treated as verbatim. When " + "`True`, URLs will not be auto-converted into links and " + "channel names will not be auto-converted into links." + ), + ) + + +SlackTextObjectType = SlackPlainTextObject | SlackMrkdwnTextObject +"""A type alias for Slack Block Kit text objects.""" + + +class SlackOptionObject(BaseModel): + """An option object for Slack Block Kit elements. + + Typically used with `SlackStaticSelectElement`. + + Reference: https://api.slack.com/reference/block-kit/composition-objects#option + """ + + # Check boxes and radio buttons could use SlackMrkdwnTextObject + text: SlackPlainTextObject = Field( + ..., + description=( + "A plain text object that defines the text shown in the option. " + "Maximum length of 75 characters." + ), + max_length=75, + ) + + value: str = Field( + ..., + description=( + "A unique string value that will be passed to your app when this " + "option is selected." + ), + max_length=150, + ) + + # Check boxes and radio buttons could use SlackMrkdwnTextObject + description: SlackPlainTextObject | None = Field( + None, + description=( + "A plain text object that defines a line of descriptive text " + "shown below the text. Maximum length of 75 characters." + ), + max_length=75, + ) + + url: str | None = Field( + None, + description=( + "A URL to load in the user's browser when the option is clicked. " + "The url attribute is only available in overflow menus. The url " + "attribute is only available in overflow menus. Maximum length of " + "3000 characters." + ), + max_length=3000, + ) + + +class SlackOptionGroupObject(BaseModel): + """An option group object for Slack Block Kit elements. + + Typically used with `SlackStaticSelectElement`. + + Reference: https://api.slack.com/reference/block-kit/composition-objects#option_group + """ + + label: SlackPlainTextObject = Field( + ..., + description=( + "A plain text object that defines the label shown above this " + "group of options. Maximum length of 75 characters." + ), + max_length=75, + ) + + options: list[SlackOptionObject] = Field( + ..., + description=( + "An array of option objects that belong to this specific group." + ), + min_length=1, + max_length=100, + ) + + +class SlackConfirmationDialogObject(BaseModel): + """A confirmation dialog object for Slack Block Kit elements. + + Reference: https://api.slack.com/reference/block-kit/composition-objects#confirm + """ + + title: SlackPlainTextObject = Field( + ..., + description=( + "A plain text object that defines the dialog's title. Maximum " + "length of 100 characters." + ), + max_length=100, + ) + + text: SlackPlainTextObject = Field( + ..., + description=( + "A text object that defines the explanatory text that appears in " + "the confirm dialog. Maximum length of 300 characters." + ), + max_length=300, + ) + + confirm: SlackPlainTextObject = Field( + ..., + description=( + "A plain text object that defines the text of the button that " + "confirms the action. Maximum length of 30 characters." + ), + max_length=30, + ) + + deny: SlackPlainTextObject = Field( + ..., + description=( + "A plain text object that defines the text of the button that " + "denies the action. Maximum length of 30 characters." + ), + max_length=30, + ) + + style: Literal["primary", "danger"] = Field( + "primary", + description=( + "A string value to determine the color of the confirm button. " + "Options include `primary` and `danger`." + ), + ) + + +class SlackStaticSelectElement(BaseModel): + """A static select element for Slack Block Kit. + + Reference: https://api.slack.com/reference/block-kit/block-elements#static_select + """ + + type: Literal["static_select"] = Field( + "static_select", + description=( + "The type of element. Reference: " + "https://api.slack.com/reference/block-kit/block-elements" + ), + ) + + placeholder: SlackPlainTextObject | None = Field( + None, + description=( + "A plain text object that defines the placeholder text shown on " + "the static select element. Maximum length of 150 characters." + ), + max_length=150, + min_length=1, + ) + + options: list[SlackOptionObject] | None = Field( + None, + description=( + "An array of option objects that populate the static select menu." + ), + min_length=1, + max_length=100, + ) + + option_groups: list[SlackOptionGroupObject] | None = Field( + None, + description=( + "An array of option group objects that populate the select menu " + "with groups of options." + ), + ) + + action_id: str = Field( + ..., + description=( + "An identifier for the action triggered when a menu option is " + "selected." + ), + max_length=255, + ) + + initial_option: SlackOptionObject | None = Field( + None, + description=( + "A single option that exactly matches one of the options within " + "options. This option will be selected when the menu initially " + "loads." + ), + ) + + confirm: SlackConfirmationDialogObject | None = Field( + None, + description=( + "A confirmation dialog that appears after a menu item is selected." + ), + ) + + focus_on_load: bool = Field( + False, + description=( + "A boolean value indicating whether the element should be " + "pre-focused when the view opens." + ), + ) + + @model_validator(mode="after") + def validate_options_or_option_groups(self) -> Self: + """Ensure that either `options` or `option_groups` is provided.""" + if not self.options and not self.option_groups: + raise ValueError( + "Either `options` or `option_groups` must be provided." + ) + if self.options and self.option_groups: + raise ValueError( + "Only one of `options` or `option_groups` can be provided." + ) + return self + + +class SlackPlainTextInputElement(BaseModel): + """A plain text input element for Slack Block Kit. + + Works with `SlackInputBlock`. + + Reference: https://api.slack.com/reference/block-kit/block-elements#input + """ + + type: Literal["plain_text_input"] = Field( + "plain_text_input", description="The type of element." + ) + + action_id: str = Field( + ..., + description=( + "An identifier for the input's value when the parent modal is " + "submitted. This should be unique among all other action_ids used " + "in the containing block. Maximum length of 255 characters." + ), + max_length=255, + ) + + initial_value: str | None = Field( + None, + description=( + "The initial (default) value in the plain-text input when it is " + "loaded." + ), + ) + + placeholder: SlackPlainTextObject | None = Field( + None, + description=( + "A plain text object that defines the placeholder text shown in " + "the plain-text input. Maximum length of 150 characters." + ), + max_length=150, + min_length=1, + ) + + multiline: bool = Field( + False, + description=( + "A boolean value indicating whether the input will be a single " + "line (false) or a larger textarea (true)." + ), + ) + + min_length: int | None = Field( + None, + description=( + "The minimum length of input that the user must provide." + ), + ge=1, + le=3000, + ) + + max_length: int | None = Field( + None, + description=("The maximum length of input that the user can provide."), + ge=1, + le=3000, + ) + + focus_on_load: bool = Field( + False, + description=( + "A boolean value indicating whether the element should be " + "pre-focused when the view opens." + ), + ) + + # dispatch_action_config is not implemented yet. + + @model_validator(mode="after") + def validate_min_max_length(self) -> Self: + """Ensure that the min_length is less than or equal to max_length.""" + if ( + self.min_length + and self.max_length + and self.min_length > self.max_length + ): + raise ValueError( + "The min_length must be less than or equal to max_length." + ) + return self + + +SlackBlock = SlackSectionBlock | SlackContextBlock | SlackInputBlock +"""A generic type alias for Slack Block Kit blocks.""" + +SlackSectionBlockAccessoryTypes = SlackStaticSelectElement +"""A type alias for Slack Block Kit section block accessory types.""" diff --git a/src/templatebot/storage/slack/variablesmodal.py b/src/templatebot/storage/slack/variablesmodal.py new file mode 100644 index 0000000..6fc75f6 --- /dev/null +++ b/src/templatebot/storage/slack/variablesmodal.py @@ -0,0 +1,254 @@ +"""Storage model for the Slack modal configuring template variables.""" + +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field, HttpUrl +from templatekit.repo import FileTemplate, ProjectTemplate + +from templatebot.constants import TEMPLATE_VARIABLES_MODAL_CALLBACK_ID + +from .blockkit import ( + SlackBlock, + SlackInputBlock, + SlackOptionGroupObject, + SlackOptionObject, + SlackPlainTextInputElement, + SlackPlainTextObject, + SlackStaticSelectElement, +) +from .views import SlackModalView + +__all__ = ["TemplateVariablesModal", "TemplateVariablesModalMetadata"] + + +class TemplateVariablesModalMetadata(BaseModel): + """Private metadata for the template variables modal. + + This metadata is encoded as JSON. It includes information about what + template is being configured. + """ + + type: Literal["file", "project"] = Field( + description="The type of template being configured." + ) + + template_name: str = Field( + description="The name of the template being configured." + ) + + git_ref: str = Field( + description="The Git reference (branch or tag) for the template." + ) + + repo_url: HttpUrl = Field( + description="The URL of the repository where the template is stored." + ) + + trigger_message_ts: str | None = Field( + description=( + "The timestamp of the message that triggered the modal (i.e., the " + "message that the user clicked on to open the modal). " + "Templatebot will update this message with the results of the " + "modal and project creation." + ) + ) + + trigger_channel_id: str | None = Field( + description=( + "The channel ID of the message that triggered the modal. " + "Used with `trigger_message_ts` to update the source message." + ) + ) + + +class TemplateVariablesModal(SlackModalView): + """A modal for configuring template variables. + + The callback ID for this modal is + `templatebot.constants.TEMPLATE_VARIABLES_MODAL_CALLBACK_ID` and the + private metadata is JSON-encoded `TemplateVariablesModalMetadata`. + """ + + @classmethod + def create( + cls, + *, + template: FileTemplate | ProjectTemplate, + git_ref: str, + repo_url: str, + trigger_message_ts: str | None = None, + trigger_channel_id: str | None = None, + ) -> TemplateVariablesModal: + """Create a modal for configuring template variables.""" + blocks = cls._create_template_modal(template) + modal_metadata = TemplateVariablesModalMetadata( + type="file" if isinstance(template, FileTemplate) else "project", + template_name=template.name, + git_ref=git_ref, + repo_url=HttpUrl(repo_url), + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + return cls( + title=SlackPlainTextObject(text=template.config["dialog_title"]), + submit=SlackPlainTextObject(text="Submit"), + close=SlackPlainTextObject(text="Cancel"), + blocks=blocks, + callback_id=TEMPLATE_VARIABLES_MODAL_CALLBACK_ID, + private_metadata=modal_metadata.model_dump_json(), + ) + + @classmethod + def _create_template_modal( + cls, + template: FileTemplate | ProjectTemplate, + ) -> list[SlackBlock]: + """Create a modal for configuring a template.""" + blocks: list[SlackBlock] = [] + for field in template.config["dialog_fields"]: + if field["component"] == "select": + if "preset_options" in field: + # Handle preset menu + element = cls._generate_preset_options_input(field) + elif "preset_groups" in field: + # Handle group preset menu + element = cls._generate_preset_groups_input(field) + else: + # Handle regular select menu + element = cls._generate_select_input(field) + else: + element = cls._generate_text_input( + field, multiline=field["component"] == "textarea" + ) + blocks.append(element) + return blocks + + @staticmethod + def _generate_text_input( + field: dict[str, Any], + *, + multiline: bool = False, + ) -> SlackInputBlock: + """Generate a text input for a default field type for the template + creation modal. + """ + return SlackInputBlock( + label=SlackPlainTextObject(text=field["label"]), + block_id=field["key"], # TODO(jonathansick): have a common prefix? + element=SlackPlainTextInputElement( + action_id=field["key"], + placeholder=SlackPlainTextObject( + text=field["placeholder"], + ) + if "placeholder" in field and len(field["placeholder"]) > 0 + else None, + multiline=multiline, + ), + hint=SlackPlainTextObject(text=field["hint"]) + if "hint" in field + else None, + optional=field.get("optional", False), + ) + + @staticmethod + def _generate_select_input(field: dict[str, Any]) -> SlackInputBlock: + """Generate an select element input block for a template creation + modal. + """ + return SlackInputBlock( + label=SlackPlainTextObject(text=field["label"]), + block_id=field["key"], + element=SlackStaticSelectElement( + action_id=field["key"], + placeholder=SlackPlainTextObject( + text=field["placeholder"], + ) + if "placeholder" in field and len(field["placeholder"]) > 0 + else None, + options=[ + SlackOptionObject( + text=SlackPlainTextObject(text=v["label"]), + value=v["value"], + # note individual options can now have descriptions + ) + for v in field["options"] + ], + ), + hint=SlackPlainTextObject(text=field["hint"]) + if "hint" in field + else None, + optional=field.get("optional", False), + ) + + @staticmethod + def _generate_preset_options_input( + field: dict[str, Any], + ) -> SlackInputBlock: + """Generate the select element input blocks for a ``preset_options`` + flavour of field in a template creation modal. + """ + return SlackInputBlock( + label=SlackPlainTextObject(text=field["label"]), + block_id=field["label"], + element=SlackStaticSelectElement( + action_id=field["label"], + placeholder=SlackPlainTextObject( + text=field["placeholder"], + ) + if "placeholder" in field and len(field["placeholder"]) > 0 + else None, + options=[ + SlackOptionObject( + text=SlackPlainTextObject(text=v["label"]), + value=v["value"], + # note individual options can now have descriptions + ) + for v in field["preset_options"] + ], + ), + hint=SlackPlainTextObject(text=field["hint"]) + if "hint" in field + else None, + optional=field.get("optional", False), + ) + + @staticmethod + def _generate_preset_groups_input( + field: dict[str, Any], + ) -> SlackInputBlock: + """Generate the select input element for a ``preset_groups`` flavor of + field in a template creation modal. + """ + option_groups: list[SlackOptionGroupObject] = [] + for group in field["preset_groups"]: + menu_group = SlackOptionGroupObject( + label=SlackPlainTextObject(text=group["group_label"]), + options=[ + SlackOptionObject( + text=SlackPlainTextObject(text=group_option["label"]), + value=group_option["label"], + ) + for group_option in group["options"] + ], + ) + option_groups.append(menu_group) + + return SlackInputBlock( + label=SlackPlainTextObject(text=field["label"]), + block_id=field["label"], + element=SlackStaticSelectElement( + action_id=field["label"], + placeholder=SlackPlainTextObject( + text=field["placeholder"], + ) + if "placeholder" in field and len(field["placeholder"]) > 0 + else None, + option_groups=option_groups, + ), + hint=SlackPlainTextObject(text=field["hint"]) + if "hint" in field + else None, + optional=field.get("optional", False), + ) diff --git a/src/templatebot/storage/slack/views.py b/src/templatebot/storage/slack/views.py new file mode 100644 index 0000000..1292960 --- /dev/null +++ b/src/templatebot/storage/slack/views.py @@ -0,0 +1,77 @@ +"""Models for Slack views (including modals).""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field + +from .blockkit import SlackBlock, SlackPlainTextObject + +__all__ = ["SlackModalView"] + + +class SlackModalView(BaseModel): + """Slack modal view.""" + + type: Literal["modal"] = Field("modal", description="The type of view.") + + title: SlackPlainTextObject = Field( + ..., + description="The title of the view. Maximum length is 24 characters.", + max_length=24, + ) + + blocks: list[SlackBlock] = Field( + description="The blocks that make up the view." + ) + + close: SlackPlainTextObject | None = Field( + None, + description=( + "The text for the close button. Maximum length is 24 characters." + ), + max_length=24, + ) + + submit: SlackPlainTextObject | None = Field( + None, + description=( + "The text for the submit button. Maximum length is 24 characters." + ), + max_length=24, + ) + + private_metadata: str | None = Field( + None, + description=( + "A string that will be sent to your app when the view is " + "submitted." + ), + max_length=3000, + ) + + callback_id: str | None = Field( + None, + description=( + "An identifier for the view. Maximum length is 255 characters." + ), + max_length=255, + ) + + clear_on_close: bool = Field( + False, + description="Whether the view should be cleared when it's closed.", + ) + + notify_on_close: bool = Field( + False, + description=( + "Whether your app should be notified when the view is closed with " + "the `view_closed` event." + ), + ) + + external_id: str | None = Field( + None, description="A unique identifier for the view." + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 08a642a..6b2007d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,34 @@ -import pytest +"""Test fixtures for unfurlbot tests.""" -from templatebot.app import create_app +from __future__ import annotations +from collections.abc import AsyncIterator -@pytest.fixture -async def client(aiohttp_client): - app = create_app() - client = await aiohttp_client(app) - return client +import httpx +import pytest_asyncio +from asgi_lifespan import LifespanManager +from fastapi import FastAPI +from httpx import AsyncClient + +from templatebot import main + + +@pytest_asyncio.fixture +async def app() -> AsyncIterator[FastAPI]: + """Return a configured test application. + + Wraps the application in a lifespan manager so that startup and shutdown + events are sent during test execution. + """ + async with LifespanManager(main.app): + yield main.app + + +@pytest_asyncio.fixture +async def client(app: FastAPI) -> AsyncIterator[AsyncClient]: + """Return an ``httpx.AsyncClient`` configured to talk to the test app.""" + transport = httpx.ASGITransport(app=app) + async with AsyncClient( + transport=transport, base_url="https://example.com/" + ) as client: + yield client diff --git a/tests/data/templates/README.md b/tests/data/templates/README.md new file mode 100644 index 0000000..2374b05 --- /dev/null +++ b/tests/data/templates/README.md @@ -0,0 +1,3 @@ +# Mock templates repository + +This repo is modelled after https://github.com/lsst/templates to test generating Slack messages based on templatekit modelling of a templates repository. diff --git a/tests/data/templates/file_templates/copyright/cookiecutter.json b/tests/data/templates/file_templates/copyright/cookiecutter.json new file mode 100644 index 0000000..6be50e4 --- /dev/null +++ b/tests/data/templates/file_templates/copyright/cookiecutter.json @@ -0,0 +1,14 @@ +{ + "copyright_year": "{% now 'utc', '%Y' %}", + "copyright_holder": [ + "Association of Universities for Research in Astronomy, Inc. (AURA)", + "Brookhaven Science Associates, LLC", + "California Institute of Technology", + "Fermi Research Alliance, LLC", + "The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory", + "The Trustees of Princeton University", + "University of Illinois Board of Trustees", + "University of Washington" + ], + "_extensions": ["jinja2_time.TimeExtension"] +} diff --git a/tests/data/templates/file_templates/copyright/templatekit.yaml b/tests/data/templates/file_templates/copyright/templatekit.yaml new file mode 100644 index 0000000..5f217ba --- /dev/null +++ b/tests/data/templates/file_templates/copyright/templatekit.yaml @@ -0,0 +1,6 @@ +name: "COPYRIGHT file" +dialog_title: "Create a COPYRIGHT" +dialog_fields: + - label: "Copyright holder" + key: "copyright_holder" + component: "select" diff --git a/tests/data/templates/project_templates/safir_fastapi_app/cookiecutter.json b/tests/data/templates/project_templates/safir_fastapi_app/cookiecutter.json new file mode 100644 index 0000000..3828777 --- /dev/null +++ b/tests/data/templates/project_templates/safir_fastapi_app/cookiecutter.json @@ -0,0 +1,20 @@ +{ + "name": "example", + "module_name": "{{ cookiecutter.name | lower | replace('-', '') | replace('_', '') | replace('.', '') }}", + "summary": "Short one-sentence summary of the app", + "copyright_year": "{% now 'utc', '%Y' %}", + "copyright_holder": [ + "Association of Universities for Research in Astronomy, Inc. (AURA)", + "California Institute of Technology", + "The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory", + "The Trustees of Princeton University", + "University of Illinois Board of Trustees", + "University of Washington" + ], + "github_org": ["lsst-sqre", "lsst-dm", "lsst-sqre-testing"], + "flavor": ["Default", "UWS"], + "_extensions": [ + "jinja2_time.TimeExtension", + "templatekit.TemplatekitExtension" + ] +} diff --git a/tests/data/templates/project_templates/safir_fastapi_app/templatekit.yaml b/tests/data/templates/project_templates/safir_fastapi_app/templatekit.yaml new file mode 100644 index 0000000..de2bab3 --- /dev/null +++ b/tests/data/templates/project_templates/safir_fastapi_app/templatekit.yaml @@ -0,0 +1,23 @@ +name: "FastAPI application (Safir)" +group: "SQuaRE" +dialog_title: "Create an application" +dialog_fields: + - key: "name" + label: "Repository name" + hint: "Name of the Git repository." + component: "text" + - key: "summary" + label: "Summary" + hint: "One sentence summary of the app (for GitHub repo and README)." + component: "text" + - key: "github_org" + label: "GitHub organization" + hint: "The package will be created in this GitHub organization." + component: "select" + - key: "flavor" + label: "Flavor" + hint: "Flavor of FastAPI service to create." + component: "select" + - label: "Initial copyright holder" + key: "copyright_holder" + component: "select" diff --git a/tests/data/templates/project_templates/stack_package/cookiecutter.json b/tests/data/templates/project_templates/stack_package/cookiecutter.json new file mode 100644 index 0000000..20e2c88 --- /dev/null +++ b/tests/data/templates/project_templates/stack_package/cookiecutter.json @@ -0,0 +1,36 @@ +{ + "package_name": "example", + "stack_name": ["LSST Science Pipelines", "None"], + "copyright_year": "{% now 'utc', '%Y' %}", + "copyright_holder": [ + "Association of Universities for Research in Astronomy, Inc. (AURA)", + "Brookhaven Science Associates, LLC", + "California Institute of Technology", + "Fermi Research Alliance, LLC", + "The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory", + "The Trustees of Princeton University", + "University of Illinois Board of Trustees", + "University of Washington" + ], + "github_org": [ + "lsst", + "lsst-dm", + "lsst-sims", + "lsst-ts", + "lsst-sitcom", + "lsst-sqre", + "lsst-sqre-testing", + "lsst-verification-and-validation" + ], + "base_package": ["base", "sconsUtils"], + "uses_cpp": ["True", "False"], + "uses_python": ["True", "False"], + "has_tasks": ["True", "False"], + "uses_dds": ["True", "False"], + "python_module": "{{ 'lsst.%s' | format(cookiecutter.package_name) | replace('_', '.') }}", + "python_sub_dirs": "{{ cookiecutter.python_module | replace('.', '/') }}", + "_extensions": [ + "jinja2_time.TimeExtension", + "templatekit.TemplatekitExtension" + ] +} diff --git a/tests/data/templates/project_templates/stack_package/templatekit.yaml b/tests/data/templates/project_templates/stack_package/templatekit.yaml new file mode 100644 index 0000000..60209c4 --- /dev/null +++ b/tests/data/templates/project_templates/stack_package/templatekit.yaml @@ -0,0 +1,143 @@ +name: "LSST EUPS package" +dialog_title: "Create a package" +dialog_fields: + - key: "package_name" + label: "Package name" + hint: "Name of EUPS package & Git repo. Use `_` for namespace hierarchy." + component: "text" + - key: "github_org" + label: "GitHub organization" + hint: "The package will be created in this GitHub organization." + component: "select" + - label: "Initial copyright holder" + key: "copyright_holder" + component: "select" + - label: "Flavor" + component: "select" + preset_groups: + - group_label: "LSST Science Pipelines" + options: + - label: "Pipelines Python" + presets: + stack_name: "LSST Science Pipelines" + base_package: "base" + uses_python: "True" + has_tasks: "False" + uses_cpp: "False" + uses_dds: "False" + - label: "Pipelines Python + Tasks" + presets: + stack_name: "LSST Science Pipelines" + base_package: "base" + uses_python: "True" + has_tasks: "False" + uses_cpp: "False" + uses_dds: "False" + - label: "Pipelines C++ + Python + Tasks" + presets: + stack_name: "LSST Science Pipelines" + base_package: "base" + uses_python: "True" + has_tasks: "True" + uses_cpp: "True" + uses_dds: "False" + - label: "Pipelines C++ + Python" + presets: + stack_name: "LSST Science Pipelines" + base_package: "base" + uses_python: "True" + has_tasks: "False" + uses_cpp: "True" + uses_dds: "False" + - label: "Pipelines C++" + presets: + stack_name: "LSST Science Pipelines" + base_package: "base" + uses_python: "False" + has_tasks: "False" + uses_cpp: "True" + uses_dds: "False" + - group_label: "Standalone" + options: + - label: "Standalone Python" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "True" + has_tasks: "False" + uses_cpp: "False" + uses_dds: "False" + - label: "Standalone Python + Tasks" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "True" + has_tasks: "False" + uses_cpp: "False" + uses_dds: "False" + - label: "Standalone C++ + Python + Tasks" + presets: + stack_name: "None" + base_package: "base" + uses_python: "sconsUtils" + has_tasks: "True" + uses_cpp: "True" + uses_dds: "False" + - label: "Standalone C++ + Python" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "True" + has_tasks: "False" + uses_cpp: "True" + uses_dds: "False" + - label: "Standalone C++" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "False" + has_tasks: "False" + uses_cpp: "True" + uses_dds: "False" + - group_label: "DDS Package" + options: + - label: "DDS Python" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "True" + has_tasks: "False" + uses_cpp: "False" + uses_dds: "True" + - label: "DDS Python + Tasks" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "True" + has_tasks: "False" + uses_cpp: "False" + uses_dds: "True" + - label: "DDS C++ + Python + Tasks" + presets: + stack_name: "None" + base_package: "base" + uses_python: "sconsUtils" + has_tasks: "True" + uses_cpp: "True" + uses_dds: "True" + - label: "DDS C++ + Python" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "True" + has_tasks: "False" + uses_cpp: "True" + uses_dds: "True" + - label: "DDS C++" + presets: + stack_name: "None" + base_package: "sconsUtils" + uses_python: "False" + has_tasks: "False" + uses_cpp: "True" + uses_dds: "True" diff --git a/tests/data/templates/project_templates/technote_rst/cookiecutter.json b/tests/data/templates/project_templates/technote_rst/cookiecutter.json new file mode 100644 index 0000000..6381caf --- /dev/null +++ b/tests/data/templates/project_templates/technote_rst/cookiecutter.json @@ -0,0 +1,54 @@ +{ + "author_id": "ID from lsst-texmf/etc/authordb.yaml", + "series": [ + "DMTN", + "ITTN", + "RTN", + "PSTN", + "SMTN", + "SITCOMTN", + "SQR", + "TSTN", + "TESTN" + ], + "serial_number": "000", + "title": "Document Title", + "repo_name": "{{ cookiecutter.series.lower() }}-{{ cookiecutter.serial_number }}", + "github_org": [ + "lsst", + "lsst-dm", + "lsst-it", + "lsst-pst", + "lsst-sims", + "lsst-sitcom", + "lsst-sqre", + "lsst-tstn", + "lsst-sqre-testing" + ], + "github_namespace": "{{ cookiecutter.github_org }}/{{ cookiecutter.repo_name }}", + "docushare_url": "", + "url": "https://{{ cookiecutter.series.lower() }}-{{ cookiecutter.serial_number }}.lsst.io", + "description": "A short description of this document", + "copyright_year": "{% now 'utc', '%Y' %}", + "copyright_holder": [ + "Association of Universities for Research in Astronomy, Inc. (AURA)", + "Brookhaven Science Associates, LLC", + "California Institute of Technology", + "Fermi Research Alliance, LLC", + "The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory", + "The Trustees of Princeton University", + "University of Illinois Board of Trustees", + "University of Washington" + ], + "first_author_given": "", + "first_author_family": "", + "first_author_orcid": "", + "first_author_affil_name": "", + "first_author_affil_internal_id": "", + "first_author_affil_address": "", + "_copy_without_render": ["*.bib"], + "_extensions": [ + "jinja2_time.TimeExtension", + "templatekit.TemplatekitExtension" + ] +} diff --git a/tests/data/templates/project_templates/technote_rst/templatekit.yaml b/tests/data/templates/project_templates/technote_rst/templatekit.yaml new file mode 100644 index 0000000..a8173d0 --- /dev/null +++ b/tests/data/templates/project_templates/technote_rst/templatekit.yaml @@ -0,0 +1,71 @@ +name: "Technote (reStructuredText)" +group: "Documents" +dialog_title: "Create an rst technote" +dialog_fields: + - key: "title" + label: "Technote title" + placeholder: "" + hint: "Don't include the document's handle and avoid rst markup." + component: "textarea" + - key: "description" + label: "Abstract or description" + hint: "You can use reStructuredText here." + component: "textarea" + placeholder: "" + - key: "author_id" + label: "First author's ID" + hint: "ID is a key from lsst-texmf's authordb.yaml (http://ls.st/uyr)" + component: "text" + placeholder: "" + - label: "Series" + component: "select" + preset_options: + - label: "DMTN" + value: "dmtn" + presets: + series: "DMTN" + github_org: "lsst-dm" + - label: "ITTN" + value: "ittn" + presets: + series: "ITTN" + github_org: "lsst-it" + - label: "PSTN" + value: "PSTN" + presets: + series: "PSTN" + github_org: "lsst-pst" + org: "PST" + - label: "RTN" + value: "rtn" + presets: + series: "RTN" + github_org: "lsst" + - label: "SITCOMTN" + value: "sitcomtn" + presets: + series: "SITCOMTN" + github_org: "lsst-sitcom" + - label: "SMTN" + value: "smtn" + presets: + series: "SMTN" + github_org: "lsst-sims" + - label: "SQR" + value: "sqr" + presets: + series: "SQR" + github_org: "lsst-sqre" + - label: "TSTN" + value: "tstn" + presets: + series: "TSTN" + github_org: "lsst-tstn" + - label: "Test" + value: "test" + presets: + series: "TESTN" + github_org: "lsst-sqre-testing" + - label: "Initial copyright holder" + key: "copyright_holder" + component: "select" diff --git a/tests/handlers/__init__.py b/tests/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/handlers/internal_test.py b/tests/handlers/internal_test.py new file mode 100644 index 0000000..1b1726e --- /dev/null +++ b/tests/handlers/internal_test.py @@ -0,0 +1,21 @@ +"""Test the internal API handlers.""" + +from __future__ import annotations + +import pytest +from httpx import AsyncClient + +from templatebot.config import config + + +@pytest.mark.asyncio +async def test_get_index(client: AsyncClient) -> None: + """Test ``GET /``.""" + response = await client.get("/") + assert response.status_code == 200 + data = response.json() + assert data["name"] == config.name + assert isinstance(data["version"], str) + assert isinstance(data["description"], str) + assert isinstance(data["repository_url"], str) + assert isinstance(data["documentation_url"], str) diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/storage/slack/__init__.py b/tests/storage/slack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/storage/slack/blockkit_test.py b/tests/storage/slack/blockkit_test.py new file mode 100644 index 0000000..2fe652b --- /dev/null +++ b/tests/storage/slack/blockkit_test.py @@ -0,0 +1,40 @@ +"""Tests for Block Kit models.""" + +from __future__ import annotations + +import pytest +from pydantic import BaseModel, Field, ValidationError + +from templatebot.storage.slack import blockkit + + +def test_plain_text_object_length() -> None: + """Test that the length of a plain text object is correct.""" + + class Model(BaseModel): + text: blockkit.SlackPlainTextObject = Field(..., max_length=5) + + data = Model.model_validate( + {"text": {"type": "plain_text", "text": "Hello"}} + ) + assert data.text.text == "Hello" + assert data.text.type == "plain_text" + + with pytest.raises(ValidationError): + Model.model_validate( + {"text": {"type": "plain_text", "text": "Hello!"}} + ) + + +def test_mrkdwn_text_object_length() -> None: + """Test that the length of a mrkdwn text object is correct.""" + + class Model(BaseModel): + text: blockkit.SlackMrkdwnTextObject = Field(..., max_length=5) + + data = Model.model_validate({"text": {"type": "mrkdwn", "text": "Hello"}}) + assert data.text.text == "Hello" + assert data.text.type == "mrkdwn" + + with pytest.raises(ValidationError): + Model.model_validate({"text": {"type": "mrkdwn", "text": "Hello!"}}) diff --git a/tests/storage/slack/variablesmodal_test.py b/tests/storage/slack/variablesmodal_test.py new file mode 100644 index 0000000..1d021bf --- /dev/null +++ b/tests/storage/slack/variablesmodal_test.py @@ -0,0 +1,137 @@ +"""Test for the variablesmodal module.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from templatekit.repo import FileTemplate, ProjectTemplate + +from templatebot.storage.slack.variablesmodal import ( + TemplateVariablesModal, + TemplateVariablesModalMetadata, +) + + +@pytest.fixture +def templates_dir() -> Path: + return Path(__file__).parent.parent.parent / "data" / "templates" + + +def test_template_variables_modal_for_copyright(templates_dir: Path) -> None: + """Test the TemplateVariablesModal with the copyright template.""" + template = FileTemplate( + str(templates_dir / "file_templates" / "copyright") + ) + git_ref = "main" + repo_url = "https://github.com/lsst/templates" + trigger_message_ts = "1234567890.123456" + trigger_channel_id = "C12345678" + + modal = TemplateVariablesModal.create( + template=template, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + + # Check that the metadata can be loaded from the modal + assert modal.private_metadata is not None + metadata = TemplateVariablesModalMetadata.model_validate_json( + modal.private_metadata + ) + assert metadata.type == "file" + + # Check that the modal has the expected title + assert modal.title.text == "Create a COPYRIGHT" + + # Check that the modal has the expected blocks + assert len(modal.blocks) == 1 + assert modal.blocks[0].type == "input" + assert modal.blocks[0].element.type == "static_select" + assert modal.blocks[0].element.options is not None + assert ( + modal.blocks[0].element.options[0].text.text + == "Association of Universities for Research in Astronomy, Inc. (AURA)" + ) + + +def test_template_variables_modal_for_fastapi(templates_dir: Path) -> None: + """Test the TemplateVariablesModal with the FastAPI template.""" + template = ProjectTemplate( + str(templates_dir / "project_templates" / "safir_fastapi_app") + ) + git_ref = "main" + repo_url = "https://github.com/lsst/templates" + trigger_message_ts = "1234567890.123456" + trigger_channel_id = "C12345678" + + modal = TemplateVariablesModal.create( + template=template, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + + # Check that the metadata can be loaded from the modal + assert modal.private_metadata is not None + metadata = TemplateVariablesModalMetadata.model_validate_json( + modal.private_metadata + ) + assert metadata.type == "project" + + +def test_template_variables_modal_for_technote(templates_dir: Path) -> None: + """Test the TemplateVariablesModal with the technote template.""" + template = ProjectTemplate( + str(templates_dir / "project_templates" / "technote_rst") + ) + git_ref = "main" + repo_url = "https://github.com/lsst/templates" + trigger_message_ts = "1234567890.123456" + trigger_channel_id = "C12345678" + + modal = TemplateVariablesModal.create( + template=template, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + + # Check that the metadata can be loaded from the modal + assert modal.private_metadata is not None + metadata = TemplateVariablesModalMetadata.model_validate_json( + modal.private_metadata + ) + assert metadata.type == "project" + + +def test_template_variables_modal_for_stack_package( + templates_dir: Path, +) -> None: + """Test the TemplateVariablesModal with the stack_package template.""" + template = ProjectTemplate( + str(templates_dir / "project_templates" / "stack_package") + ) + git_ref = "main" + repo_url = "https://github.com/lsst/templates" + trigger_message_ts = "1234567890.123456" + trigger_channel_id = "C12345678" + + modal = TemplateVariablesModal.create( + template=template, + git_ref=git_ref, + repo_url=repo_url, + trigger_message_ts=trigger_message_ts, + trigger_channel_id=trigger_channel_id, + ) + + # Check that the metadata can be loaded from the modal + assert modal.private_metadata is not None + metadata = TemplateVariablesModalMetadata.model_validate_json( + modal.private_metadata + ) + assert metadata.type == "project" diff --git a/tests/test_healthcheck.py b/tests/test_healthcheck.py deleted file mode 100644 index 092c64d..0000000 --- a/tests/test_healthcheck.py +++ /dev/null @@ -1,5 +0,0 @@ -async def test_healthcheck(): - # TODO we need a simulated Kafka in order to actually test as it stands now - # response = await client.get("/") - # assert response.status == 200 - pass diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 64a304e..0000000 --- a/tox.ini +++ /dev/null @@ -1,36 +0,0 @@ -[tox] -envlist = py,coverage-report,lint -isolated_build = True - -[testenv] -description = Run pytest against {envname}. -deps = - -r{toxinidir}/requirements/main.txt - -r{toxinidir}/requirements/dev.txt -commands = - pytest --cov=templatebot --cov-branch --cov-report= {posargs} - -[testenv:coverage-report] -description = Compile coverage from each test run. -skip_install = true -deps = coverage[toml]>=5.0.2 -depends = - py -commands = coverage report - -[testenv:typing] -description = Run mypy. -commands = - mypy src tests setup.py - -[testenv:lint] -description = Lint codebase by running pre-commit (Black, isort, Flake8). -skip_install = true -deps = - pre-commit -commands = pre-commit run --all-files - -[testenv:run] -description = Run the development server with auto-reload for code changes. -usedevelop = true -commands = adev runserver --app-factory create_app src/templatebot/app.py