From 8ae50b543eb78f2173c66acc2e9b676a7026f80a Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 1 Feb 2024 04:12:05 +0100 Subject: [PATCH 01/81] feat: Initialize project --- .devcontainer/Dockerfile | 46 - .devcontainer/devcontainer.json | 65 - .env.example | 2 + .github/dependabot.yml | 25 - .github/workflows/codeql.yml | 54 - .github/workflows/dev.yml | 69 - .github/workflows/main.yml | 67 - .gitignore | 201 +- .pre-commit-config.yaml | 20 - .vscode/settings.json | 10 - Dockerfile | 19 - LICENSE | 661 ---- README.md | 44 - SECURITY.md | 11 - api/__init__.py | 27 - api/announcements.py | 242 -- api/backends/__init__.py | 0 api/backends/backend.py | 91 - api/backends/entities.py | 173 -- api/backends/github.py | 455 --- api/compat.py | 88 - api/donations.py | 38 - api/github.py | 220 -- api/info.py | 32 - api/login.py | 52 - api/manager.py | 61 - api/models/__init__.py | 0 api/models/announcements.py | 40 - api/models/compat.py | 49 - api/models/donations.py | 39 - api/models/github.py | 129 - api/models/info.py | 40 - api/models/manager.py | 32 - api/models/socials.py | 23 - api/ping.py | 24 - api/robots.py | 11 - api/socials.py | 32 - api/utils/__init__.py | 0 api/utils/auth.py | 40 - api/utils/http_utils.py | 26 - api/utils/limiter.py | 7 - api/utils/versioning.py | 8 - app.py | 87 - build.gradle.kts | 47 + config.py | 152 - data/database.py | 6 - data/models.py | 77 - docker-compose.yml | 16 - docs/.swagger-codegen-ignore | 23 + docs/.swagger-codegen/VERSION | 1 + docs/index.html | 2752 +++++++++++++++++ gradle.properties | 4 + gradle/libs.versions.toml | 41 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 ++ gradlew.bat | 89 + mypy.ini | 30 - persistence/.gitkeep | 0 poetry.lock | 2599 ---------------- pyproject.toml | 45 - requirements.txt | 62 - settings.gradle.kts | 7 + .../kotlin/app/revanced/api/Application.kt | 24 + .../app/revanced/api/backend/Backend.kt | 140 + .../api/backend/github/GitHubBackend.kt | 116 + .../api/backend/github/api/RequestResource.kt | 26 + .../api/backend/github/api/ResponseSchema.kt | 52 + .../app/revanced/api/plugins/Databases.kt | 49 + .../app/revanced/api/plugins/Dependencies.kt | 21 + .../kotlin/app/revanced/api/plugins/HTTP.kt | 37 + .../app/revanced/api/plugins/Routing.kt | 45 + .../app/revanced/api/plugins/Security.kt | 30 + .../app/revanced/api/plugins/Serialization.kt | 11 + .../app/revanced/api/plugins/UsersSchema.kt | 59 + src/main/resources/logback.xml | 12 + src/main/resources/openapi/documentation.yaml | 23 + src/main/resources/static/about.json | 99 + src/main/resources/static/robots.txt | 2 + .../kotlin/app/revanced/ApplicationTest.kt | 21 + static/img/favicon.ico | Bin 20239 -> 0 bytes 81 files changed, 4010 insertions(+), 6307 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .env.example delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/dev.yml delete mode 100644 .github/workflows/main.yml delete mode 100644 .pre-commit-config.yaml delete mode 100644 .vscode/settings.json delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 SECURITY.md delete mode 100644 api/__init__.py delete mode 100644 api/announcements.py delete mode 100644 api/backends/__init__.py delete mode 100644 api/backends/backend.py delete mode 100644 api/backends/entities.py delete mode 100644 api/backends/github.py delete mode 100644 api/compat.py delete mode 100644 api/donations.py delete mode 100644 api/github.py delete mode 100644 api/info.py delete mode 100644 api/login.py delete mode 100644 api/manager.py delete mode 100644 api/models/__init__.py delete mode 100644 api/models/announcements.py delete mode 100644 api/models/compat.py delete mode 100644 api/models/donations.py delete mode 100644 api/models/github.py delete mode 100644 api/models/info.py delete mode 100644 api/models/manager.py delete mode 100644 api/models/socials.py delete mode 100644 api/ping.py delete mode 100644 api/robots.py delete mode 100644 api/socials.py delete mode 100644 api/utils/__init__.py delete mode 100644 api/utils/auth.py delete mode 100644 api/utils/http_utils.py delete mode 100644 api/utils/limiter.py delete mode 100644 api/utils/versioning.py delete mode 100644 app.py create mode 100644 build.gradle.kts delete mode 100644 config.py delete mode 100644 data/database.py delete mode 100644 data/models.py delete mode 100644 docker-compose.yml create mode 100644 docs/.swagger-codegen-ignore create mode 100644 docs/.swagger-codegen/VERSION create mode 100644 docs/index.html create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat delete mode 100644 mypy.ini delete mode 100644 persistence/.gitkeep delete mode 100644 poetry.lock delete mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 settings.gradle.kts create mode 100644 src/main/kotlin/app/revanced/api/Application.kt create mode 100644 src/main/kotlin/app/revanced/api/backend/Backend.kt create mode 100644 src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt create mode 100644 src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt create mode 100644 src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/Databases.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/Dependencies.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/HTTP.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/Routing.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/Security.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/Serialization.kt create mode 100644 src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt create mode 100644 src/main/resources/logback.xml create mode 100644 src/main/resources/openapi/documentation.yaml create mode 100644 src/main/resources/static/about.json create mode 100644 src/main/resources/static/robots.txt create mode 100644 src/test/kotlin/app/revanced/ApplicationTest.kt delete mode 100644 static/img/favicon.ico diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 7ef7d028..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/base:jammy -# FROM mcr.microsoft.com/devcontainers/base:jammy - -ARG DEBIAN_FRONTEND=noninteractive -ARG USER=vscode - -RUN DEBIAN_FRONTEND=noninteractive \ - && apt-get update \ - && apt-get install -y build-essential --no-install-recommends make \ - ca-certificates \ - git \ - libssl-dev \ - zlib1g-dev \ - libbz2-dev \ - libreadline-dev \ - libsqlite3-dev \ - wget \ - curl \ - llvm \ - libncurses5-dev \ - xz-utils \ - tk-dev \ - libxml2-dev \ - libxmlsec1-dev \ - libffi-dev \ - liblzma-dev \ - && rm -rf /var/lib/apt/lists/* - -# Python and poetry installation -USER $USER -ARG HOME="/home/$USER" -ARG PYTHON_VERSION=3.11 -# ARG PYTHON_VERSION=3.10 - -ENV PYENV_ROOT="${HOME}/.pyenv" -ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${HOME}/.local/bin:$PATH" - -RUN echo "done 0" \ - && curl https://pyenv.run | bash \ - && echo "done 1" \ - && pyenv install ${PYTHON_VERSION} \ - && echo "done 2" \ - && pyenv global ${PYTHON_VERSION} \ - && echo "done 3" \ - && curl -sSL https://install.python-poetry.org | python3 - \ - && poetry config virtualenvs.in-project true diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 08905421..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "poetry3-poetry-pyenv", - "build": { - "dockerfile": "Dockerfile" - }, - - // 👇 Features to add to the Dev Container. More info: https://containers.dev/implementors/features. - // "features": {}, - - // 👇 Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // 👇 Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "", - - // 👇 Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python", - "njpwerner.autodocstring", - "ms-azuretools.vscode-docker", - "github.copilot-labs", - "github.copilot-nightly", - "eamodio.gitlens", - "visualstudioexptteam.intellicode-api-usage-examples", - "ms-python.isort", - "ms-vsliveshare.vsliveshare", - "matangover.mypy", - "ms-python.vscode-pylance", - "mgesbert.python-path", - "zeshuaro.vscode-python-poetry", - "njqdev.vscode-python-typehint", - "ms-python.black-formatter" - ] - } - }, - "features": { - "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/sshd:1": {}, - "ghcr.io/devcontainers-contrib/features/black:2": {}, - "ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {}, - "ghcr.io/devcontainers-contrib/features/ffmpeg-apt-get:1": {}, - "ghcr.io/devcontainers-contrib/features/micro:1": {}, - "ghcr.io/devcontainers-contrib/features/mosh-apt-get:1": {}, - "ghcr.io/devcontainers-contrib/features/mypy:2": {}, - "ghcr.io/devcontainers-contrib/features/poetry:2": {}, - "ghcr.io/devcontainers-contrib/features/wget-apt-get:1": {}, - "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}, - "ghcr.io/jckimble/devcontainer-features/ngrok:3": {}, - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": true, - "configureZshAsDefaultShell": true, - "installOhMyZsh": true, - "upgradePackages": true, - "username": "codespace", - "userUid": "automatic", - "userGid": "automatic" - } - } - - // 👇 Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..6129bbb8 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +GITHUB_TOKEN= +API_VERSION= \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f02748cd..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - target-branch: "dev" - assignees: - - "alexandreteles" - - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - target-branch: "dev" - assignees: - - "alexandreteles" - - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" - target-branch: "dev" - assignees: - - "alexandreteles" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 5373d7e8..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [dev] - pull_request: - types: [opened, reopened, edited, synchronize] - schedule: - - cron: "29 5 * * 5" - workflow_dispatch: - -jobs: - analyze: - name: Analyze - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - actions: read - contents: read - security-events: write - strategy: - fail-fast: false - matrix: - language: ["python"] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11.7" - - - name: Install project dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; - then pip install -r requirements.txt; - fi - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index 91b030c3..00000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Build dev branch - -on: - push: - branches: [ "dev" ] - schedule: - - cron: '24 9 * * 6' - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - IMAGE_TAG: ${{ github.sha }} - -jobs: - security_checks: - runs-on: ubuntu-latest - name: Security check - steps: - - uses: actions/checkout@v4 - - name: Security Checks (PyCharm Security) - uses: tonybaloney/pycharm-security@master - with: - path: . - - build: - needs: security_checks - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout Dockerfile - id: checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.branch }} - - - name: Setup QEMU - id: qemu - uses: docker/setup-qemu-action@v3 - with: - image: tonistiigi/binfmt:latest - platforms: all - - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - flavor: | - latest=${{ startsWith(github.ref, 'refs/heads/main') }} - suffix=-${{ github.sha }} - - - name: Build Docker image - id: build - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64/v8 - push: false - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 886e0ace..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Build and Publish Docker Image - -on: - push: - branches: [main] - schedule: - - cron: "24 9 * * 6" - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - IMAGE_TAG: ${{ github.sha }} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout Dockerfile - id: checkout - uses: actions/checkout@v4 - - - name: Setup QEMU - id: qemu - uses: docker/setup-qemu-action@v3 - with: - image: tonistiigi/binfmt:latest - platforms: all - - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - id: ghcr - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GH_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - flavor: | - latest=${{ startsWith(github.ref, 'refs/heads/main') }} - suffix=-${{ github.sha }} - - - name: Build and push main Docker image - id: build - uses: docker/build-push-action@v5 - with: - build-args: GH_TOKEN=${{ secrets.GH_TOKEN }} - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64/v8 - cache-to: type=gha,mode=max,ignore-error=true - cache-from: type=gha - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index b8beef64..d16fa9ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,164 +1,39 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python +.gradle build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# custom -env.sh -persistence/database.db +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Project ### +.env \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 952f0211..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: check-docstring-first - - id: debug-statements - - id: requirements-txt-fixer - - id: check-toml - - id: check-merge-conflict - - repo: https://github.com/psf/black - rev: 24.3.0 - hooks: - - id: black - language_version: python3.11 -ci: - autoupdate_branch: "dev" diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ad9bb995..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "python.analysis.typeCheckingMode": "off", - "spellright.language": [ - "pt" - ], - "spellright.documentTypes": [ - "markdown", - "latex" - ] -} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8c044a72..00000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.11-slim - -ARG GITHUB_TOKEN -ARG SENTRY_DSN - -ENV GITHUB_TOKEN $GITHUB_TOKEN -ENV SENTRY_DSN $SENTRY_DSN - -WORKDIR /usr/src/app - -COPY . . - -RUN apt update && \ - apt-get install git build-essential libffi-dev libssl-dev openssl --no-install-recommends -y \ - && pip install --no-cache-dir -r requirements.txt - -VOLUME persistence - -CMD [ "python3", "-m" , "sanic", "app:app", "--fast", "--access-logs", "--motd", "--noisy-exceptions", "-H", "0.0.0.0"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0ad25db4..00000000 --- a/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/README.md b/README.md deleted file mode 100644 index aaa86ee8..00000000 --- a/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# ReVanced Releases API - ---- - -![License: AGPLv3](https://img.shields.io/github/license/revanced/revanced-api) -[![Build and Publish Docker Image](https://github.com/revanced/revanced-api/actions/workflows/main.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/main.yml) - ---- - -This is a simple API that proxies requests needed to feed the ReVanced Manager and website with data. - -## Usage - -To run this API, you need Python 3.11.x. You can install the dependencies with poetry: - -```shell -poetry install -``` - -Create the following environment variables: - -- `GITHUB_TOKEN` with a valid GitHub token with read access to public repositories -- `SECRET_KEY` to salt login sessions -- `USERNAME` & `PASSWORD` to initialize the database with a user to login with to authenticated endpoints - -Then, you can run the API in development mode with: - -```shell -poetry run sanic app:app --dev -``` - -or in production mode with: - -```shell -poetry run sanic app:app --fast -``` - -## Contributing - -If you want to contribute to this project, feel free to open a pull request or an issue. We don't do much here, so it's pretty easy to contribute. - -## License - -This project is licensed under the AGPLv3 License - see the [LICENSE](LICENSE) file for details. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index a96b0b4e..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,11 +0,0 @@ -# Security Policy - -## Supported Tags - -| Tag | ReVanced Version | -| ------- | ------------------ | -| latest | latest upstream | - -## Reporting a Vulnerability - -To report a vulnerability, please open an Issue in our issue tracker here on GitHub. diff --git a/api/__init__.py b/api/__init__.py deleted file mode 100644 index 1ce029b7..00000000 --- a/api/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# api/__init__.py -from sanic import Blueprint -import importlib -import pkgutil -from api.utils.versioning import get_version - -# Dynamically import all modules in the 'api' package, excluding subdirectories -versioned_blueprints: dict[str, list] = {} -for finder, module_name, ispkg in pkgutil.iter_modules(["api"]): - if not ispkg: - # Import the module - module = importlib.import_module(f"api.{module_name}") - - # Add the module's blueprint to the versioned list, if it exists - if hasattr(module, module_name): - blueprint = getattr(module, module_name) - version = get_version(module_name) - versioned_blueprints.setdefault(version, []).append(blueprint) - -# Create Blueprint groups for each version -api = [] -for version, blueprints in versioned_blueprints.items(): - if version == "old" or version == "v0": - group = Blueprint.group(*blueprints, url_prefix="/") - else: - group = Blueprint.group(*blueprints, version=version, url_prefix="/") - api.append(group) diff --git a/api/announcements.py b/api/announcements.py deleted file mode 100644 index bcdc4c06..00000000 --- a/api/announcements.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -This module provides a blueprint for the announcements endpoint. - -Routes: - - GET /announcements: Get a list of announcements from all channels. - - GET /announcements/: Get a list of announcement from a channel. - - GET /announcements/latest: Get the latest announcement. - - GET /announcements//latest: Get the latest announcement from a channel. - - POST /announcements/: Create an announcement. - - DELETE /announcements/: Delete an announcement. -""" - -import os -import datetime -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi -from data.database import Session -from data.models import AnnouncementDbModel, AttachmentDbModel - -import sanic_beskar - -from api.models.announcements import AnnouncementResponseModel -from api.utils.limiter import limiter - -announcements: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - - -@announcements.get("/announcements") -@openapi.definition( - summary="Get a list of announcements", - response=[[AnnouncementResponseModel]], -) -async def get_announcements(request: Request) -> JSONResponse: - """ - Retrieve a list of announcements. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing a list of announcements from all channels. - """ - - session = Session() - - announcements = [ - AnnouncementResponseModel.to_response(announcement) - for announcement in session.query(AnnouncementDbModel).all() - ] - - session.close() - - return json(announcements, status=200) - - -@announcements.get("/announcements/") -@openapi.definition( - summary="Get a list of announcements from a channel", - response=[[AnnouncementResponseModel]], -) -async def get_announcements_for_channel(request: Request, channel: str) -> JSONResponse: - """ - Retrieve a list of announcements from a channel. - - **Args:** - - channel (str): The channel to retrieve announcements from. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing a list of announcements from a channel. - """ - - session = Session() - - announcements = [ - AnnouncementResponseModel.to_response(announcement) - for announcement in session.query(AnnouncementDbModel) - .filter_by(channel=channel) - .all() - ] - - session.close() - - return json(announcements, status=200) - - -@announcements.get("/announcements/latest") -@openapi.definition( - summary="Get the latest announcement", - response=AnnouncementResponseModel, -) -async def get_latest_announcement(request: Request) -> JSONResponse: - """ - Retrieve the latest announcement. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the latest announcement. - """ - - session = Session() - - announcement = ( - session.query(AnnouncementDbModel) - .order_by(AnnouncementDbModel.id.desc()) - .first() - ) - - if not announcement: - return json({"error": "No announcement found"}, status=404) - - announcement_response = AnnouncementResponseModel.to_response(announcement) - - session.close() - - return json(announcement_response, status=200) - - -# for specific channel - - -@announcements.get("/announcements//latest") -@openapi.definition( - summary="Get the latest announcement from a channel", - response=AnnouncementResponseModel, -) -async def get_latest_announcement_for_channel( - request: Request, channel: str -) -> JSONResponse: - """ - Retrieve the latest announcement from a channel. - - **Args:** - - channel (str): The channel to retrieve the latest announcement from. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the latest announcement from a channel. - """ - - session = Session() - - announcement = ( - session.query(AnnouncementDbModel) - .filter_by(channel=channel) - .order_by(AnnouncementDbModel.id.desc()) - .first() - ) - - if not announcement: - return json({"error": "No announcement found"}, status=404) - - announcement_response = AnnouncementResponseModel.to_response(announcement) - - session.close() - - return json(announcement_response, status=200) - - -@announcements.post("/announcements/") -@limiter.limit("16 per hour") -@sanic_beskar.auth_required -@openapi.definition( - summary="Create an announcement", - body=AnnouncementResponseModel, - response=AnnouncementResponseModel, -) -async def post_announcement(request: Request, channel: str) -> JSONResponse: - """ - Create an announcement. - - **Args:** - - author (str | None): The author of the announcement. - - title (str): The title of the announcement. - - content (ContentFields | None): The content of the announcement. - - channel (str): The channel to create the announcement in. - - nevel (int | None): The severity of the announcement. - """ - session = Session() - - if not request.json: - return json({"error": "Missing request body"}, status=400) - - content = request.json.get("content", None) - - author = request.json.get("author", None) - title = request.json.get("title") - message = content["message"] if content and "message" in content else None - attachments = ( - list( - map( - lambda url: AttachmentDbModel(attachment_url=url), - content["attachments"], - ) - ) - if content and "attachments" in content - else [] - ) - level = request.json.get("level", None) - created_at = datetime.datetime.now() - - announcement = AnnouncementDbModel( - author=author, - title=title, - message=message, - attachments=attachments, - channel=channel, - created_at=created_at, - level=level, - ) - - session.add(announcement) - session.commit() - session.close() - - return json({}, status=200) - - -@announcements.delete("/announcements/") -@sanic_beskar.auth_required -@openapi.definition( - summary="Delete an announcement", -) -async def delete_announcement(request: Request, announcement_id: int) -> JSONResponse: - """ - Delete an announcement. - - **Args:** - - announcement_id (int): The ID of the announcement to delete. - - **Exceptions:** - - 404: Announcement not found. - """ - session = Session() - - announcement = ( - session.query(AnnouncementDbModel).filter_by(id=announcement_id).first() - ) - - if not announcement: - return json({"error": "Announcement not found"}, status=404) - - session.delete(announcement) - session.commit() - session.close() - - return json({}, status=200) diff --git a/api/backends/__init__.py b/api/backends/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/backends/backend.py b/api/backends/backend.py deleted file mode 100644 index 24dfc63f..00000000 --- a/api/backends/backend.py +++ /dev/null @@ -1,91 +0,0 @@ -from abc import abstractmethod -from typing import Any, Protocol - -from api.backends.entities import * - - -class Backend(Protocol): - """Interface for a generic backend. - - Attributes: - name (str): Name of the backend. - base_url (str): Base URL of the backend. - - Methods: - list_releases: Retrieve a list of releases. - get_release_by_tag_name: Retrieve a release by its tag name. - get_latest_release: Retrieve the latest release. - get_latest_pre_release: Retrieve the latest pre-release. - get_release_notes: Retrieve the release notes of a specific release. - get_contributors: Retrieve the list of contributors. - get_patches: Retrieve the patches of a specific release. - """ - - name: str - base_url: str - - def __init__(self, name: str, base_url: str): - self.name = name - self.base_url = base_url - - @abstractmethod - async def list_releases(self, *args: Any, **kwargs: Any) -> list[Release]: - raise NotImplementedError - - @abstractmethod - async def get_release_by_tag_name(self, *args: Any, **kwargs: Any) -> Release: - raise NotImplementedError - - @abstractmethod - async def get_latest_release(self, *args: Any, **kwargs: Any) -> Release: - raise NotImplementedError - - @abstractmethod - async def get_latest_pre_release(self, *args: Any, **kwargs: Any) -> Release: - raise NotImplementedError - - @abstractmethod - async def get_contributors(self, *args: Any, **kwargs: Any) -> list[Contributor]: - raise NotImplementedError - - @abstractmethod - async def get_patches(self, *args: Any, **kwargs: Any) -> list[dict]: - raise NotImplementedError - - @abstractmethod - async def get_team_members(self, *args: Any, **kwargs: Any) -> list[Contributor]: - raise NotImplementedError - - -class Repository: - """A repository that communicates with a specific backend. - - Attributes: - backend (Backend): The backend instance used to communicate with the repository. - """ - - def __init__(self, backend: Backend): - self.backend = backend - - -class AppInfoProvider(Protocol): - """Interface for a generic app info provider. - - Attributes: - name (str): Name of the app info provider. - base_url (str): Base URL of the app info provider. - - Methods: - get_app_info: Retrieve information about an app. - """ - - name: str - base_url: str - - def __init__(self, name: str, base_url: str): - self.name = name - self.base_url = base_url - - @abstractmethod - async def get_app_info(self, *args: Any, **kwargs: Any) -> AppInfo: - raise NotImplementedError diff --git a/api/backends/entities.py b/api/backends/entities.py deleted file mode 100644 index 1cb022f6..00000000 --- a/api/backends/entities.py +++ /dev/null @@ -1,173 +0,0 @@ -from typing import Optional -from dataclasses import dataclass - - -@dataclass -class Metadata(dict): - """ - Represents the metadata of a release. - - Attributes: - - tag_name (str): The name of the release tag. - - name (str): The name of the release. - - body (str): The body of the release. - - draft (bool): Whether the release is a draft. - - prerelease (bool): Whether the release is a prerelease. - - created_at (str): The creation date of the release. - - published_at (str): The publication date of the release. - """ - - def __init__( - self, - tag_name: str, - name: str, - draft: bool, - prerelease: bool, - created_at: str, - published_at: str, - body: str, - repository: Optional[str] = None, - ): - dict.__init__( - self, - tag_name=tag_name, - name=name, - draft=draft, - prerelease=prerelease, - created_at=created_at, - published_at=published_at, - body=body, - repository=repository, - ) - - -@dataclass -class Asset(dict): - """ - Represents an asset in a release. - - Attributes: - - name (str): The name of the asset. - - content_type (str): The MIME type of the asset content. - - download_count (int): The number of times the asset has been downloaded. - - download_url (str): The URL to download the asset. - """ - - def __init__( - self, - name: str, - content_type: str, - download_count: int, - browser_download_url: str, - ): - dict.__init__( - self, - name=name, - content_type=content_type, - download_count=download_count, - browser_download_url=browser_download_url, - ) - - -@dataclass -class Release(dict): - """ - Represents a release. - - Attributes: - - metadata (Metadata): The metadata of the release. - - assets (list[Asset]): The assets of the release. - """ - - def __init__(self, metadata: Metadata, assets: list[Asset]): - dict.__init__(self, metadata=metadata, assets=assets) - - -@dataclass -class Contributor(dict): - """ - Represents a contributor to a repository. - - Attributes: - - login (str): The GitHub username of the contributor. - - avatar_url (str): The URL to the contributor's avatar image. - - html_url (str): The URL to the contributor's GitHub profile. - - contributions (Optional[int]): The number of contributions the contributor has made to the repository. - """ - - def __init__( - self, - login: str, - avatar_url: str, - html_url: str, - contributions: Optional[int] = None, - bio: Optional[str] = None, - keys: Optional[str] = None, - ): - match contributions, bio, keys: - case None, None, None: - dict.__init__( - self, - login=login, - avatar_url=avatar_url, - html_url=html_url, - bio=bio, - keys=keys, - ) - case int(_), None, None: - dict.__init__( - self, - login=login, - avatar_url=avatar_url, - html_url=html_url, - contributions=contributions, - ) - case None, str(_), None: - dict.__init__( - self, - login=login, - avatar_url=avatar_url, - html_url=html_url, - bio=bio, - ) - case int(_), str(_), str(_): - dict.__init__( - self, - login=login, - avatar_url=avatar_url, - html_url=html_url, - contributions=contributions, - bio=bio, - keys=keys, - ) - case None, str(_), str(_): - dict.__init__( - self, - login=login, - avatar_url=avatar_url, - html_url=html_url, - bio=bio, - keys=keys, - ) - case _: - raise ValueError("Invalid arguments") - - -@dataclass -class AppInfo(dict): - """ - Represents the information of an app. - - Attributes: - - name (str): The name of the app. - - category (str): The app category. - - logo (str): The base64 enconded app logo. - """ - - def __init__(self, name: str, category: str, logo: str): - dict.__init__( - self, - name=name, - category=category, - logo=logo, - ) diff --git a/api/backends/github.py b/api/backends/github.py deleted file mode 100644 index 82a5c452..00000000 --- a/api/backends/github.py +++ /dev/null @@ -1,455 +0,0 @@ -import asyncio -import os -from operator import eq -from typing import Any, Optional - -import ujson -from aiohttp import ClientResponse -from sanic import SanicException -from cytoolz import filter, map, partial, curry, pipe -from cytoolz.dicttoolz import get_in, keyfilter -from cytoolz.itertoolz import mapcat, pluck - -from api.backends.backend import Backend, Repository -from api.backends.entities import * -from api.backends.entities import Contributor -from api.utils.http_utils import http_get - -repo_name: str = "github" -base_url: str = "https://api.github.com" - - -class GithubRepository(Repository): - """ - A repository class that represents a GitHub repository. - - Args: - owner (str): The username of the owner of the GitHub repository. - name (str): The name of the GitHub repository. - """ - - def __init__(self, owner: str, name: str): - """ - Initializes a new instance of the GithubRepository class. - - Args: - owner (str): The username of the owner of the GitHub repository. - name (str): The name of the GitHub repository. - """ - super().__init__(Github()) - self.owner = owner - self.name = name - - -class Github(Backend): - """ - A backend class that interacts with the GitHub API. - - Attributes: - name (str): The name of the GitHub backend. - base_url (str): The base URL of the GitHub API. - token (str): The GitHub access token used for authentication. - headers (dict[str, str]): The HTTP headers to be sent with each request to the GitHub API. - """ - - def __init__(self): - """ - Initializes a new instance of the GitHub class. - """ - super().__init__(repo_name, base_url) - self.token: Optional[str] = os.getenv("GITHUB_TOKEN") - self.headers: dict[str, str] = { - "Authorization": f"Bearer {self.token}", - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - } - self.repositories_rest_endpoint: str = f"{base_url}/repos" - - @staticmethod - async def __assemble_release(release: dict) -> Release: - async def __assemble_asset(asset: dict) -> Asset: - asset_data: dict = keyfilter( - lambda key: key - in {"name", "content_type", "download_count", "browser_download_url"}, - asset, - ) - return Asset(**asset_data) - - filter_metadata = keyfilter( - lambda key: key - in { - "tag_name", - "name", - "draft", - "prerelease", - "created_at", - "published_at", - "body", - }, - release, - ) - metadata = Metadata(**filter_metadata) - assets = await asyncio.gather(*map(__assemble_asset, release["assets"])) - return Release(metadata=metadata, assets=assets) - - @staticmethod - async def __assemble_contributor( - contributor: dict, team_view: bool = False - ) -> Contributor: - match team_view: - case True: - keys = {"login", "avatar_url", "html_url", "bio"} - case _: - keys = {"login", "avatar_url", "html_url", "contributions"} - - filter_contributor = keyfilter( - lambda key: key in keys, - contributor, - ) - - if team_view: - filter_contributor["keys"] = ( - f"{base_url.replace('api.', '')}/{filter_contributor['login']}.gpg" - ) - - return Contributor(**filter_contributor) - - @staticmethod - async def __validate_request(_response: ClientResponse) -> None: - if _response.status != 200: - raise SanicException( - context=await _response.json(loads=ujson.loads), - status_code=_response.status, - ) - - async def list_releases( - self, repository: GithubRepository, per_page: int = 30, page: int = 1 - ) -> list[Release]: - """ - Returns a list of Release objects for a given GitHub repository. - - Args: - repository (GithubRepository): The GitHub repository for which to retrieve the releases. - per_page (int): The number of releases to return per page. - page (int): The page number of the releases to return. - - Returns: - list[Release]: A list of Release objects. - """ - list_releases_endpoint: str = ( - f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases?per_page={per_page}&page={page}" - ) - response: ClientResponse = await http_get( - headers=self.headers, url=list_releases_endpoint - ) - await self.__validate_request(response) - releases: list[Release] = await asyncio.gather( - *map( - lambda release: self.__assemble_release(release), - await response.json(loads=ujson.loads), - ) - ) - return releases - - async def get_release_by_tag_name( - self, repository: GithubRepository, tag_name: str - ) -> Release: - """ - Retrieves a specific release for a given GitHub repository by its tag name. - - Args: - repository (GithubRepository): The GitHub repository for which to retrieve the release. - tag_name (str): The tag name of the release to retrieve. - - Returns: - Release: The Release object representing the retrieved release. - """ - release_by_tag_endpoint: str = ( - f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases/tags/{tag_name}" - ) - response: ClientResponse = await http_get( - headers=self.headers, url=release_by_tag_endpoint - ) - await self.__validate_request(response) - return await self.__assemble_release(await response.json(loads=ujson.loads)) - - async def get_latest_release( - self, - repository: GithubRepository, - ) -> Release: - """Get the latest release for a given repository. - - Args: - repository (GithubRepository): The GitHub repository for which to retrieve the release. - - Returns: - Release: The latest release for the given repository. - """ - latest_release_endpoint: str = ( - f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases/latest" - ) - response: ClientResponse = await http_get( - headers=self.headers, url=latest_release_endpoint - ) - await self.__validate_request(response) - return await self.__assemble_release(await response.json(loads=ujson.loads)) - - async def get_latest_pre_release( - self, - repository: GithubRepository, - ) -> Release: - """Get the latest pre-release for a given repository. - - Args: - repository (GithubRepository): The GitHub repository for which to retrieve the release. - - Returns: - Release: The latest pre-release for the given repository. - """ - list_releases_endpoint: str = ( - f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases?per_page=10&page=1" - ) - response: ClientResponse = await http_get( - headers=self.headers, url=list_releases_endpoint - ) - await self.__validate_request(response) - latest_pre_release = next( - filter( - lambda release: release["prerelease"], - await response.json(loads=ujson.loads), - ) - ) - return await self.__assemble_release(latest_pre_release) - - async def get_contributors(self, repository: GithubRepository) -> list[Contributor]: - """Get a list of contributors for a given repository. - - Args: - repository (GithubRepository): The repository for which to retrieve contributors. - - Returns: - list[Contributor]: A list of contributors for the given repository. - """ - - contributors_endpoint: str = ( - f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/contributors" - ) - response: ClientResponse = await http_get( - headers=self.headers, url=contributors_endpoint - ) - await self.__validate_request(response) - contributors: list[Contributor] = await asyncio.gather( - *map(self.__assemble_contributor, await response.json(loads=ujson.loads)) - ) - - return contributors - - async def get_patches( - self, repository: GithubRepository, tag_name: str = "latest", dev: bool = False - ) -> list[dict]: - """Get a dictionary of patch URLs for a given repository. - - Args: - repository (GithubRepository): The repository for which to retrieve patches. - tag_name (str): The name of the release tag. - dev (bool): If we should get the latest pre-release instead. - - Returns: - list[dict]: A JSON object containing the patches. - """ - - async def __fetch_download_url(_release: Release) -> str: - asset = get_in(["assets"], _release) - patch_asset = next( - filter(lambda x: eq(get_in(["name"], x), "patches.json"), asset), None - ) - return get_in(["browser_download_url"], patch_asset) - - match tag_name: - case "latest": - match dev: - case True: - release = await self.get_latest_pre_release(repository) - case _: - release = await self.get_latest_release(repository) - case _: - release = await self.get_release_by_tag_name( - repository=repository, tag_name=tag_name - ) - - response: ClientResponse = await http_get( - headers=self.headers, - url=await __fetch_download_url(_release=release), - ) - await self.__validate_request(response) - return ujson.loads(await response.read()) - - async def get_team_members(self, repository: GithubRepository) -> list[Contributor]: - """Get the list of team members from the owner organization of a given repository. - - Args: - repository (GithubRepository): The repository for which to retrieve team members in the owner organization. - - Returns: - list[Contributor]: A list of members in the owner organization. - """ - team_members_endpoint: str = f"{self.base_url}/orgs/{repository.owner}/members" - user_info_endpoint: str = f"{self.base_url}/users/" - response: ClientResponse = await http_get( - headers=self.headers, url=team_members_endpoint - ) - await self.__validate_request(response) - logins: list[str] = list(pluck("login", await response.json())) - _http_get = partial(http_get, headers=self.headers) - user_data_response: list[dict] = await asyncio.gather( - *map( - lambda login: _http_get(url=f"{user_info_endpoint}{login}"), - logins, - ) - ) - user_data = await asyncio.gather( - *map( - lambda _response: _response.json(loads=ujson.loads), - user_data_response, - ) - ) - team_members: list[Contributor] = await asyncio.gather( - *map( - lambda member: self.__assemble_contributor(member, team_view=True), - list(user_data), - ) - ) - - return team_members - - async def compat_get_tools( - self, repositories: list[GithubRepository], dev: bool - ) -> list: - """Get the latest releases for a set of repositories (v1 compat). - - Args: - repositories (set[GithubRepository]): The repositories for which to retrieve releases. - dev: If we should get the latest pre-release instead. - - Returns: - list[dict[str, str]]: A JSON object containing the releases. - """ - - def transform(data: dict, repository: GithubRepository): - """Transforms a dictionary from the input list into a list of dictionaries with the desired structure. - - Args: - data(dict): A dictionary from the input list. - repository(GithubRepository): The repository for which to retrieve releases. - - Returns: - _[list]: A list of dictionaries with the desired structure. - """ - - def process_asset(asset: dict) -> dict: - """Transforms an asset dictionary into a new dictionary with the desired structure. - - Args: - asset(dict): An asset dictionary. - - Returns: - _[dict]: A new dictionary with the desired structure. - """ - return { - "repository": f"{repository.owner}/{repository.name}", - "version": data["metadata"]["tag_name"], - "timestamp": data["metadata"]["published_at"], - "name": asset["name"], - "browser_download_url": asset["browser_download_url"], - "content_type": asset["content_type"], - } - - return map(process_asset, data["assets"]) - - results = await asyncio.gather( - *map( - lambda release: self.get_latest_release(release), - repositories, - ) - ) - - return list(mapcat(lambda pair: transform(*pair), zip(results, repositories))) - - async def compat_get_contributors( - self, repositories: list[GithubRepository] - ) -> list: - """Get the contributors for a set of repositories (v1 compat). - - Args: - repositories (set[GithubRepository]): The repositories for which to retrieve contributors. - - Returns: - list[dict[str, str]]: A JSON object containing the contributors. - """ - - def transform(data: dict, repository: GithubRepository) -> dict[str, Any]: - """Transforms a dictionary from the input list into a list of dictionaries with the desired structure. - - Args: - data(dict): A dictionary from the input list. - repository(GithubRepository): The repository for which to retrieve contributors. - - Returns: - _[list]: A list of dictionaries with the desired structure. - """ - return { - "name": f"{repository.owner}/{repository.name}", - "contributors": data, - } - - results = await asyncio.gather( - *map( - lambda repository: self.get_contributors(repository), - repositories, - ) - ) - - return list(map(lambda pair: transform(*pair), zip(results, repositories))) - - async def generate_custom_sources( - self, repositories: list[GithubRepository], dev: bool - ) -> dict[str, dict[str, str]]: - """Generate a custom sources dictionary for a set of repositories. - - Args: - repositories (list[GithubRepository]): The repositories for which to generate the sources. - dev (bool): If we should get the latest pre-release instead. - - Returns: - dict[str, dict[str, str]]: A dictionary containing the custom sources. - """ - - # Helper functions - filter_by_name = curry(lambda name, item: name in item["name"]) - filter_patches_jar = curry( - lambda item: "patches" in item["name"] and item["name"].endswith(".jar") - ) - get_fields = curry( - lambda fields, item: {field: item[field] for field in fields} - ) - rename_key = curry( - lambda old, new, d: {new if k == old else k: v for k, v in d.items()} - ) - - sources = await self.compat_get_tools(repositories, dev) - - patches = pipe( - sources, - lambda items: next(filter(filter_patches_jar, items), None), - get_fields(["version", "browser_download_url"]), - rename_key("browser_download_url", "url"), - ) - - integrations = pipe( - sources, - lambda items: next(filter(filter_by_name("integrations"), items), None), - get_fields(["version", "browser_download_url"]), - rename_key("browser_download_url", "url"), - ) - - return {"patches": patches, "integrations": integrations} diff --git a/api/compat.py b/api/compat.py deleted file mode 100644 index 95207291..00000000 --- a/api/compat.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -This module provides endpoints for compatibility with the old API. - -Routes: - - GET //releases: Retrieve a list of releases for a GitHub repository. - - GET //releases/latest: Retrieve the latest release for a GitHub repository. - - GET //releases/tag/: Retrieve a specific release for a GitHub repository by its tag name. - - GET //contributors: Retrieve a list of contributors for a GitHub repository. - - GET /patches/: Retrieve a list of patches for a given release tag. - -""" - -import os -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi - -from api.backends.github import Github, GithubRepository -from api.models.github import * -from api.models.compat import ToolsResponseModel, ContributorsResponseModel -from config import compat_repositories, owner - -compat: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - -github_backend: Github = Github() - - -@compat.get("/tools") -@openapi.definition( - summary="Get patching tools' latest version.", response=[ToolsResponseModel] -) -async def tools(request: Request) -> JSONResponse: - """ - Retrieve a list of releases for a GitHub repository. - - **Args:** - - repo (str): The name of the GitHub repository to retrieve releases for. - - **Query Parameters:** - - per_page (int): The number of releases to retrieve per page. - - page (int): The page number of the releases to retrieve. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the list of releases. - - **Raises:** - - HTTPException: If there is an error retrieving the releases. - """ - - data: dict[str, list] = { - "tools": await github_backend.compat_get_tools( - repositories=[ - GithubRepository(owner=owner, name=repo) - for repo in compat_repositories - if repo - not in ["revanced-api", "revanced-releases-api", "revanced-website"] - ], - dev=True if request.args.get("dev") else False, - ) - } - - return json(data, status=200) - - -@compat.get("/contributors") -@openapi.definition( - summary="Get organization-wise contributors.", response=[ContributorsResponseModel] -) -async def contributors(request: Request) -> JSONResponse: - """ - Retrieve a list of releases for a GitHub repository. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the list of releases. - - **Raises:** - - HTTPException: If there is an error retrieving the releases. - """ - - data: dict[str, list] = { - "repositories": await github_backend.compat_get_contributors( - repositories=[ - GithubRepository(owner=owner, name=repo) for repo in compat_repositories - ] - ) - } - - return json(data, status=200) diff --git a/api/donations.py b/api/donations.py deleted file mode 100644 index 25ae875f..00000000 --- a/api/donations.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -This module provides a blueprint for the donations endpoint. - -Routes: - - GET /donations: Get ReVanced donation links and wallets. -""" - -import os - -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi - -from api.models.donations import DonationsResponseModel -from config import wallets, links - -donations: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - - -@donations.get("/donations") -@openapi.definition( - summary="Get ReVanced donation links and wallets", - response=[DonationsResponseModel], -) -async def root(request: Request) -> JSONResponse: - """ - Returns a JSONResponse with a dictionary containing ReVanced donation links and wallets. - - **Returns:** - - JSONResponse: A Sanic JSONResponse instance containing a dictionary with the donation links and wallets. - """ - data: dict[str, dict] = { - "donations": { - "wallets": wallets, - "links": links, - } - } - return json(data, status=200) diff --git a/api/github.py b/api/github.py deleted file mode 100644 index aaf98bc9..00000000 --- a/api/github.py +++ /dev/null @@ -1,220 +0,0 @@ -""" -This module provides endpoints for interacting with the GitHub API. - -Routes: - - GET //releases: Retrieve a list of releases for a GitHub repository. - - GET //releases/latest: Retrieve the latest release for a GitHub repository. - - GET //releases/tag/: Retrieve a specific release for a GitHub repository by its tag name. - - GET //contributors: Retrieve a list of contributors for a GitHub repository. - - GET /patches/: Retrieve a list of patches for a given release tag. - -""" - -import os -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi - -from api.backends.entities import Release, Contributor -from api.backends.github import Github, GithubRepository -from api.models.github import * -from config import owner, default_repository - -github: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - -github_backend: Github = Github() - - -@github.get("//releases") -@openapi.definition( - summary="Get releases for a repository", response=[ReleaseListResponseModel] -) -async def list_releases(request: Request, repo: str) -> JSONResponse: - """ - Retrieve a list of releases for a GitHub repository. - - **Args:** - - repo (str): The name of the GitHub repository to retrieve releases for. - - **Query Parameters:** - - per_page (int): The number of releases to retrieve per page. - - page (int): The page number of the releases to retrieve. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the list of releases. - - **Raises:** - - HTTPException: If there is an error retrieving the releases. - """ - - per_page = int(request.args.get("per_page")) if request.args.get("per_page") else 30 - page = int(request.args.get("page")) if request.args.get("page") else 1 - - data: dict[str, list[Release]] = { - "releases": await github_backend.list_releases( - repository=GithubRepository(owner=owner, name=repo), - per_page=per_page, - page=page, - ) - } - - return json(data, status=200) - - -@github.get("//releases/latest") -@openapi.definition( - summary="Get the latest release for a repository", - response=SingleReleaseResponseModel, -) -async def latest_release(request: Request, repo: str) -> JSONResponse: - """ - Retrieve the latest release for a GitHub repository. - - **Args:** - - repo (str): The name of the GitHub repository to retrieve the release for. - - **Query Parameters:** - - dev (bool): Whether or not to retrieve the latest development release. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the release. - - **Raises:** - - HTTPException: If there is an error retrieving the releases. - """ - - data: dict[str, Release] - - match request.args.get("dev"): - case "true": - data = { - "release": await github_backend.get_latest_pre_release( - repository=GithubRepository(owner=owner, name=repo) - ) - } - case _: - data = { - "release": await github_backend.get_latest_release( - repository=GithubRepository(owner=owner, name=repo) - ) - } - - return json(data, status=200) - - -@github.get("//releases/tag/") -@openapi.definition( - summary="Retrieve a release for a GitHub repository by its tag name.", - response=SingleReleaseResponseModel, -) -async def get_release_by_tag_name( - request: Request, repo: str, tag: str -) -> JSONResponse: - """ - Retrieve a release for a GitHub repository by its tag name. - - **Args:** - - repo (str): The name of the GitHub repository to retrieve the release for. - - tag (str): The tag for the release to be retrieved. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the release. - - **Raises:** - - HTTPException: If there is an error retrieving the releases. - """ - - data: dict[str, Release] = { - "release": await github_backend.get_release_by_tag_name( - repository=GithubRepository(owner=owner, name=repo), tag_name=tag - ) - } - - return json(data, status=200) - - -@github.get("//contributors") -@openapi.definition( - summary="Retrieve a list of contributors for a repository.", - response=ContributorsModel, -) -async def get_contributors(request: Request, repo: str) -> JSONResponse: - """ - Retrieve a list of contributors for a repository. - - **Args:** - - repo (str): The name of the GitHub repository to retrieve the contributors for. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the list of contributors. - - **Raises:** - - HTTPException: If there is an error retrieving the contributors. - """ - - data: dict[str, list[Contributor]] = { - "contributors": await github_backend.get_contributors( - repository=GithubRepository(owner=owner, name=repo) - ) - } - - return json(data, status=200) - - -@github.get("/patches/") -@openapi.definition( - summary="Retrieve a list of patches for a release.", response=PatchesModel -) -async def get_patches(request: Request, tag: str) -> JSONResponse: - """ - Retrieve a list of patches for a release. - - **Args:** - - tag (str): The tag for the patches to be retrieved. - - **Query Parameters:** - - dev (bool): Whether or not to retrieve the latest development release. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the list of patches. - - **Raises:** - - HTTPException: If there is an error retrieving the patches. - """ - - repo: str = "revanced-patches" - - dev: bool = bool(request.args.get("dev")) - - data: dict[str, list[dict]] = { - "patches": await github_backend.get_patches( - repository=GithubRepository(owner=owner, name=repo), tag_name=tag, dev=dev - ) - } - - return json(data, status=200) - - -@github.get("/team/members") -@openapi.definition( - summary="Retrieve a list of team members for the Revanced organization.", - response=TeamMembersModel, -) -async def get_team_members(request: Request) -> JSONResponse: - """ - Retrieve a list of team members for the Revanced organization. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the list of team members. - - **Raises:** - - HTTPException: If there is an error retrieving the team members. - """ - - data: dict[str, list[Contributor]] = { - "members": await github_backend.get_team_members( - repository=GithubRepository(owner=owner, name=default_repository) - ) - } - - return json(data, status=200) diff --git a/api/info.py b/api/info.py deleted file mode 100644 index 60fbeac5..00000000 --- a/api/info.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This module provides a blueprint for the info endpoint. - -Routes: - - GET /info: Get info about the owner of the API. -""" - -import os -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi - -from api.models.info import InfoResponseModel -from config import default_info - -info: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - - -@info.get("/info") -@openapi.definition( - summary="Information about the API", - response=[InfoResponseModel], -) -async def root(request: Request) -> JSONResponse: - """ - Returns a JSONResponse with a dictionary containing info about the owner of the API. - - **Returns:** - - JSONResponse: A Sanic JSONResponse instance containing a dictionary with the info about the owner of the API. - """ - data: dict[str, dict] = {"info": default_info} - return json(data, status=200) diff --git a/api/login.py b/api/login.py deleted file mode 100644 index 939960b6..00000000 --- a/api/login.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This module provides a blueprint for the login endpoint. - -Routes: - - POST /login: Login to the API -""" - -import os -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi -from sanic_beskar.exceptions import AuthenticationError - -from api.utils.auth import beskar -from api.utils.limiter import limiter - -login: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - - -@login.post("/login") -@openapi.definition( - summary="Login to the API", -) -@limiter.limit("3 per hour") -async def login_user(request: Request) -> JSONResponse: - """ - Login to the API. - - **Args:** - - username (str): The username of the user to login. - - password (str): The password of the user to login. - - **Returns:** - - JSONResponse: A Sanic JSONResponse object containing the access token. - """ - - req = request.json - username = req.get("username", None) - password = req.get("password", None) - if not username or not password: - return json({"error": "Missing username or password"}, status=400) - - try: - user = await beskar.authenticate(username, password) - except AuthenticationError: - return json({"error": "Invalid username or password"}, status=403) - - if not user: - return json({"error": "Invalid username or password"}, status=403) - - ret = {"access_token": await beskar.encode_token(user)} - return json(ret, status=200) diff --git a/api/manager.py b/api/manager.py deleted file mode 100644 index 87a3e602..00000000 --- a/api/manager.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -This module provides ReVanced Manager specific endpoints. - -Routes: - - GET /manager/bootstrap: Get a list of the main ReVanced tools. - - GET /manager/sources: Get a list of ReVanced sources. -""" - -import os -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi - -from api.backends.github import GithubRepository, Github - -from api.models.manager import BootsrapResponseModel, CustomSourcesResponseModel -from config import compat_repositories, owner - -manager: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - -github_backend: Github = Github() - - -@manager.get("/manager/bootstrap") -@openapi.definition( - summary="Get a list of the main ReVanced tools", - response=[BootsrapResponseModel], -) -async def bootstrap(request: Request) -> JSONResponse: - """ - Returns a JSONResponse with a list of the main ReVanced tools. - - **Returns:** - - JSONResponse: A Sanic JSONResponse instance containing a list with the tool names. - """ - data: dict[str, dict] = {"tools": compat_repositories} - return json(data, status=200) - - -@manager.get("/manager/custom-source") -@openapi.definition( - summary="Get a list of ReVanced sources", - response=[CustomSourcesResponseModel], -) -async def custom_sources(request: Request) -> JSONResponse: - """ - Returns a JSONResponse with a list of the main ReVanced sources. - - **Returns:** - - JSONResponse: A Sanic JSONResponse instance containing a list with the source names. - """ - data = await github_backend.generate_custom_sources( - repositories=[ - GithubRepository(owner=owner, name=repo) - for repo in compat_repositories - if "patches" in repo or "integrations" in repo - ], - dev=True if request.args.get("dev") else False, - ) - - return json(data, status=200) diff --git a/api/models/__init__.py b/api/models/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/models/announcements.py b/api/models/announcements.py deleted file mode 100644 index fd5f3df2..00000000 --- a/api/models/announcements.py +++ /dev/null @@ -1,40 +0,0 @@ -from data.models import AnnouncementDbModel - - -class ContentFields(dict): - message: str | None - attachment_urls: list[str] | None - - -class AnnouncementResponseModel(dict): - id: int - author: str | None - title: str - content: ContentFields | None - channel: str - created_at: str - level: int | None - - @staticmethod - def to_response(announcement: AnnouncementDbModel): - response = AnnouncementResponseModel( - id=announcement.id, - author=announcement.author, - title=announcement.title, - content=( - ContentFields( - message=announcement.message, - attachment_urls=[ - attachment.attachment_url - for attachment in announcement.attachments - ], - ) - if announcement.message or announcement.attachments - else None - ), - channel=announcement.channel, - created_at=str(announcement.created_at), - level=announcement.level, - ) - - return response diff --git a/api/models/compat.py b/api/models/compat.py deleted file mode 100644 index b1876136..00000000 --- a/api/models/compat.py +++ /dev/null @@ -1,49 +0,0 @@ -from pydantic import BaseModel -from api.models.github import ContributorsFields - - -class ToolsResponseFields(BaseModel): - """Implements the fields for the /tools endpoint. - - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - repository: str - version: str - timestamp: str - name: str - size: str | None = None - browser_download_url: str - content_type: str - - -class ToolsResponseModel(BaseModel): - """Implements the JSON response model for the /tools endpoint. - - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - tools: list[ToolsResponseFields] - - -class ContributorsResponseFields(BaseModel): - """Implements the fields for the /contributors endpoint. - - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - name: str - contributors: list[ContributorsFields] - - -class ContributorsResponseModel(BaseModel): - """Implements the JSON response model for the /contributors endpoint. - - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - repositories: list[ContributorsResponseFields] diff --git a/api/models/donations.py b/api/models/donations.py deleted file mode 100644 index 12ad18ba..00000000 --- a/api/models/donations.py +++ /dev/null @@ -1,39 +0,0 @@ -from pydantic import BaseModel - - -class WalletFields(BaseModel): - """ - Implements the fields for a crypto wallet. - """ - - network: str - currency_code: str - address: str - preferred: bool - - -class LinkFields(BaseModel): - """ - Implements the fields for a donation link. - """ - - name: str - url: str - preferred: bool - - -class DonationFields(BaseModel): - """ - A Pydantic BaseModel that represents all the donation links and wallets. - """ - - wallets: list[WalletFields] - links: list[LinkFields] - - -class DonationsResponseModel(BaseModel): - """ - A Pydantic BaseModel that represents a dictionary of donation links. - """ - - donations: DonationFields diff --git a/api/models/github.py b/api/models/github.py deleted file mode 100644 index e2f246af..00000000 --- a/api/models/github.py +++ /dev/null @@ -1,129 +0,0 @@ -from typing import Any, Optional -from pydantic import BaseModel - - -class MetadataFields(BaseModel): - """ - Metadata fields for a GitHub release. - """ - - tag_name: str - name: str - draft: bool - prerelease: bool - created_at: str - published_at: str - body: str - - -class AssetFields(BaseModel): - """ - Asset fields for a GitHub release. - """ - - name: str - content_type: str - download_count: int - browser_download_url: str - - -class ReleaseResponseModel(BaseModel): - """ - Response model for a GitHub release. - """ - - metadata: MetadataFields - assets: list[AssetFields] - - -class SingleReleaseResponseModel(BaseModel): - """ - Response model for a GitHub release. - """ - - release: ReleaseResponseModel - - -class ReleaseListResponseModel(BaseModel): - """ - Response model for a list of GitHub releases. - """ - - releases: list[ReleaseResponseModel] - - -class CompatiblePackagesResponseFields(BaseModel): - """ - Implements the fields for compatible packages in the PatchesResponseFields class. - """ - - name: str - versions: list[str] | None - - -class PatchesOptionsResponseFields(BaseModel): - key: str - title: str - description: str - required: bool - choices: list[Any] | None - - -class PatchesResponseFields(BaseModel): - """ - Implements the fields for the /patches endpoint. - """ - - name: str - description: str - version: str - excluded: bool - dependencies: list[str] | None - options: list[PatchesOptionsResponseFields] | None - compatiblePackages: list[CompatiblePackagesResponseFields] - - -class PatchesModel(BaseModel): - """ - Response model for a list of GitHub releases. - """ - - patches: list[PatchesResponseFields] - - -class ContributorsFields(BaseModel): - """ - Implements the fields for a contributor. - """ - - login: str - avatar_url: str - html_url: str - contributions: Optional[int] - - -class ContributorsModel(BaseModel): - """ - Response model for a list of contributors. - """ - - contributors: list[ContributorsFields] - - -class TeamMemberFields(BaseModel): - """ - Implements the fields for a team member. - """ - - login: str - avatar_url: str - html_url: str - bio: Optional[str] - - -class TeamMembersModel(BaseModel): - """ - Responde model for a list of team members. - """ - - members: list[TeamMemberFields] diff --git a/api/models/info.py b/api/models/info.py deleted file mode 100644 index 06b102c1..00000000 --- a/api/models/info.py +++ /dev/null @@ -1,40 +0,0 @@ -from api.models.donations import DonationFields -from api.models.socials import SocialFields -from pydantic import BaseModel - - -class ContactFields(BaseModel): - """ - Implements the fields for the API owner contact info. - """ - - email: str - - -class BrandingFields(BaseModel): - """ - Implements the fields for the API owner branding info. - """ - - logo: str - - -class InfoFields(BaseModel): - """ - Implements the fields for the API owner info. - """ - - name: str - about: str - branding: BrandingFields - contact: ContactFields - socials: list[SocialFields] - donations: DonationFields - - -class InfoResponseModel(BaseModel): - """ - A Pydantic BaseModel that represents a dictionary of info. - """ - - info: InfoFields diff --git a/api/models/manager.py b/api/models/manager.py deleted file mode 100644 index c7e39b3b..00000000 --- a/api/models/manager.py +++ /dev/null @@ -1,32 +0,0 @@ -from pydantic import BaseModel - - -class BootsrapResponseModel(BaseModel): - """ - A Pydantic BaseModel that represents a list of available tools. - """ - - tools: list[str] - """ - A list of available tools. - """ - - -class CustomSourcesFields(BaseModel): - """ - Implements the fields for a source. - """ - - url: str - preferred: bool - - -class CustomSourcesResponseModel(BaseModel): - """ - A Pydantic BaseModel that represents a list of available sources. - """ - - _: dict[str, CustomSourcesFields] - """ - A list of available sources. - """ diff --git a/api/models/socials.py b/api/models/socials.py deleted file mode 100644 index 45edad01..00000000 --- a/api/models/socials.py +++ /dev/null @@ -1,23 +0,0 @@ -from pydantic import BaseModel - - -class SocialFields(BaseModel): - """ - Implements the fields for a social network link. - """ - - name: str - url: str - preferred: bool - - -class SocialsResponseModel(BaseModel): - """ - A Pydantic BaseModel that represents a dictionary of social links. - """ - - socials: list[SocialFields] - """ - A dictionary where the keys are the names of the social networks, and - the values are the links to the profiles or pages. - """ diff --git a/api/ping.py b/api/ping.py deleted file mode 100644 index 09bce092..00000000 --- a/api/ping.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -This module provides endpoints for pinging the API. - -Routes: - - GET /ping: Ping the API. -""" - -import os -from sanic import Blueprint, HTTPResponse, Request, response -from sanic_ext import openapi - -ping: Blueprint = Blueprint(os.path.basename(__file__).rstrip(".py")) - - -@ping.get("/ping") -@openapi.summary("Ping the API") -async def root(request: Request) -> HTTPResponse: - """ - Endpoint for pinging the API. - - **Returns:** - - Empty response with status code 204. - """ - return response.empty(status=204) diff --git a/api/robots.py b/api/robots.py deleted file mode 100644 index 10460c32..00000000 --- a/api/robots.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -from sanic import Blueprint -from sanic.response import text - - -robots: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - - -@robots.get("/robots.txt") -async def robots_txt(request): - return text("User-agent: *\nDisallow: /", content_type="text/plain") diff --git a/api/socials.py b/api/socials.py deleted file mode 100644 index f58dcc66..00000000 --- a/api/socials.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This module provides a blueprint for the socials endpoint. - -Routes: - - GET /socials: Get ReVanced socials. -""" - -import os -from sanic import Blueprint, Request -from sanic.response import JSONResponse, json -from sanic_ext import openapi - -from api.models.socials import SocialsResponseModel -from config import social_links - -socials: Blueprint = Blueprint(os.path.basename(__file__).strip(".py")) - - -@socials.get("/socials") -@openapi.definition( - summary="Get ReVanced socials", - response=[SocialsResponseModel], -) -async def root(request: Request) -> JSONResponse: - """ - Returns a JSONResponse with a dictionary containing ReVanced social links. - - **Returns:** - - JSONResponse: A Sanic JSONResponse instance containing a dictionary with the social links. - """ - data: dict[str, dict] = {"socials": social_links} - return json(data, status=200) diff --git a/api/utils/__init__.py b/api/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/api/utils/auth.py b/api/utils/auth.py deleted file mode 100644 index f336a0f8..00000000 --- a/api/utils/auth.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import secrets -import string -from data.database import Session - -from sanic_beskar import Beskar - -from data.models import UserDbModel - -beskar = Beskar() - - -def configure_auth(app): - app.config.SECRET_KEY = os.environ.get("SECRET_KEY").join( - secrets.choice(string.ascii_letters) for i in range(15) - ) - app.config["TOKEN_ACCESS_LIFESPAN"] = {"hours": 24} - app.config["TOKEN_REFRESH_LIFESPAN"] = {"days": 30} - beskar.init_app(app, UserDbModel) - - _init_default_user() - - -def _init_default_user(): - username = os.environ.get("USERNAME") - password = os.environ.get("PASSWORD") - - if not username or not password: - raise Exception("Missing USERNAME or PASSWORD environment variables") - - session = Session() - - existing_user = session.query(UserDbModel).filter_by(username=username).first() - if not existing_user: - session.add( - UserDbModel(username=username, password=beskar.hash_password(password)) - ) - session.commit() - - session.close() diff --git a/api/utils/http_utils.py b/api/utils/http_utils.py deleted file mode 100644 index 98c9e30a..00000000 --- a/api/utils/http_utils.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Optional - -import ujson -from aiohttp import ClientSession - -_client: Optional[ClientSession] = None - - -async def http_get(headers, url): - """ - Performs a GET HTTP request to a given URL with the provided headers. - - Args: - headers (dict): A dictionary containing headers to be included in the HTTP request. - url (str): The URL to which the HTTP request will be made. - - Returns: - The HTTP response returned by the server. - """ - global _client - if _client is None: - _client = ClientSession(json_serialize=ujson.dumps) - return await _client.get(url, headers=headers) - else: - assert isinstance(_client, ClientSession) - return await _client.get(url, headers=headers) diff --git a/api/utils/limiter.py b/api/utils/limiter.py deleted file mode 100644 index 5198ca27..00000000 --- a/api/utils/limiter.py +++ /dev/null @@ -1,7 +0,0 @@ -from sanic_limiter import Limiter, get_remote_address - -limiter = Limiter(key_func=get_remote_address) - - -def configure_limiter(app): - limiter.init_app(app) diff --git a/api/utils/versioning.py b/api/utils/versioning.py deleted file mode 100644 index ad69dd4b..00000000 --- a/api/utils/versioning.py +++ /dev/null @@ -1,8 +0,0 @@ -from cytoolz import keyfilter -from config import api_versions - - -def get_version(value: str) -> str: - result = keyfilter(lambda key: value in api_versions[key], api_versions) - - return list(result.keys())[0] if result else "v0" diff --git a/app.py b/app.py deleted file mode 100644 index a699c847..00000000 --- a/app.py +++ /dev/null @@ -1,87 +0,0 @@ -# app.py - -import os -from typing import Any - -from sanic import HTTPResponse, Sanic -import sanic.response -from sanic_ext import Config - -from api import api -from config import openapi_title, openapi_version, openapi_description, hostnames - -from api.utils.limiter import configure_limiter -from api.utils.auth import configure_auth - -import sentry_sdk - -if os.environ.get("SENTRY_DSN"): - sentry_sdk.init( - dsn=os.environ["SENTRY_DSN"], - enable_tracing=True, - traces_sample_rate=1.0, - ) -else: - print("WARNING: Sentry DSN not set, not enabling Sentry") - -REDIRECTS = { - "/": "/docs/swagger", -} - -app = Sanic("revanced-api") -app.extend(config=Config(oas_ignore_head=False)) -app.ext.openapi.describe( - title=openapi_title, - version=openapi_version, - description=openapi_description, -) -app.config.CORS_ALWAYS_SEND = True -app.config.CORS_AUTOMATIC_OPTIONS = True -app.config.CORS_VARY_HEADER = True -app.config.CORS_METHODS = ["GET", "HEAD", "OPTIONS"] -app.config.CORS_SUPPORTS_CREDENTIALS = True -app.config.CORS_SEND_WILDCARD = True -app.config.CORS_ORIGINS = "*" - -# sanic-beskar -configure_auth(app) - -# sanic-limiter -configure_limiter(app) - -app.blueprint(api) - -# https://sanic.dev/en/guide/how-to/static-redirects.html - - -def get_static_function(value) -> Any: - return lambda *_, **__: value - - -for src, dest in REDIRECTS.items(): - app.route(src)(get_static_function(sanic.response.redirect(dest))) - - -@app.on_request -async def domain_check(request) -> HTTPResponse: - if request.host not in hostnames: - return sanic.response.redirect(f"https://api.revanced.app/{request.path}") - - -@app.on_response -async def add_cache_control(_, response): - response.headers["Cache-Control"] = "public, max-age=300" - - -@app.on_response -async def add_csp(_, response): - response.headers["Content-Security-Policy"] = ( - "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;" - ) - - -app.static( - "/favicon.ico", - "static/img/favicon.ico", - name="favicon", -) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..822a45fa --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.kotlin) + alias(libs.plugins.ktor) + alias(libs.plugins.serilization) +} + +group = "app.revanced" + +application { + mainClass.set("app.revanced.api.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.auth) + implementation(libs.ktor.client.resources) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.auth) + implementation(libs.ktor.server.auth.jwt) + implementation(libs.ktor.server.swagger) + implementation(libs.ktor.server.openapi) + implementation(libs.ktor.server.cors) + implementation(libs.ktor.server.caching.headers) + implementation(libs.ktor.server.host.common) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.conditional.headers) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.koin.ktor) + implementation(libs.h2) + implementation(libs.logback.classic) + implementation(libs.exposed.core) + implementation(libs.exposed.jdbc) + implementation(libs.dotenv.kotlin) + testImplementation(libs.ktor.server.tests) + testImplementation(libs.kotlin.test.junit) +} diff --git a/config.py b/config.py deleted file mode 100644 index aee86103..00000000 --- a/config.py +++ /dev/null @@ -1,152 +0,0 @@ -# API Configuration - -from typing import Any - - -backend: str = "github" -redis: dict[str, str | int] = {"host": "localhost", "port": 6379} -hostnames: list[str] = [ - "api.revanced.app", - "deimos.revanced.app", - "localhost:8000", - "127.0.0.1:8000", -] - -# GitHub Backend Configuration - -owner: str = "revanced" -default_repository: str = ".github" - -# API Versioning - -api_versions: dict[str, list[str]] = { - "old": ["compat"], - "v2": [ - "announcements", - "donations", - "github", - "info", - "login", - "ping", - "socials", - "manager", - ], -} - -api_version: str = "v2" -openapi_version: str = "2.0.0" -openapi_title: str = "ReVanced API" -openapi_description: str = """ -## The official JSON API for ReVanced Releases 🚀 - -### Links - -- [Changelogs](https://github.com/revanced/) -- [Official links to ReVanced](https://revanced.app) - -### Important Information - -* Rate Limiting - 60 requests per minute -* Cache - 5 minutes - -### Additional Notes - -1. Breaking changes are to be expected -2. Client side caching is advised to avoid unnecessary requests -3. Abuse of the API will result in IP blocks -""" - -# Testing Configuration - -github_testing_repository: str = "revanced-patches" -github_testing_tag: str = "v2.173.0" -apkdl_testing_package: str = "com.google.android.youtube" - -# Old API Configuration - -compat_api_version: str = "v1" -compat_repositories: list = [ - "revanced-patcher", - "revanced-patches", - "revanced-integrations", - "revanced-manager", - "revanced-cli", - "revanced-website", - "revanced-api", - "revanced-releases-api", -] - -# Social Links - -social_links: list[dict[str, str | bool]] = [ - {"name": "Website", "url": "https://revanced.app", "preferred": True}, - {"name": "GitHub", "url": "https://github.com/revanced", "preferred": False}, - {"name": "Twitter", "url": "https://twitter.com/revancedapp", "preferred": False}, - {"name": "Discord", "url": "https://revanced.app/discord", "preferred": True}, - { - "name": "Reddit", - "url": "https://www.reddit.com/r/revancedapp", - "preferred": False, - }, - {"name": "Telegram", "url": "https://t.me/app_revanced", "preferred": False}, - {"name": "YouTube", "url": "https://www.youtube.com/@ReVanced", "preferred": False}, -] - -# Donation info - -wallets: list[dict[str, str | bool]] = [ - { - "network": "Bitcoin", - "currency_code": "BTC", - "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f", - "preferred": False, - }, - { - "network": "Dogecoin", - "currency_code": "DOGE", - "address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp", - "preferred": True, - }, - { - "network": "Ethereum", - "currency_code": "ETH", - "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5", - "preferred": False, - }, - { - "network": "Litecoin", - "currency_code": "LTC", - "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2", - "preferred": False, - }, - { - "network": "Monero", - "currency_code": "XMR", - "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh", - "preferred": False, - }, -] - -links: list[dict[str, str | bool]] = [ - { - "name": "Open Collective", - "url": "https://opencollective.com/revanced", - "preferred": True, - }, - { - "name": "GitHub Sponsors", - "url": "https://github.com/sponsors/ReVanced", - "preferred": False, - }, -] - -default_info: dict[str, Any] = { - "name": "ReVanced", - "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", - "branding": { - "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" - }, - "contact": {"email": "contact@revanced.app"}, - "socials": social_links, - "donations": {"wallets": wallets, "links": links}, -} diff --git a/data/database.py b/data/database.py deleted file mode 100644 index ad93c569..00000000 --- a/data/database.py +++ /dev/null @@ -1,6 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -engine = create_engine("sqlite:///persistence/database.db", pool_size=20) - -Session = sessionmaker(bind=engine) diff --git a/data/models.py b/data/models.py deleted file mode 100644 index 36621baa..00000000 --- a/data/models.py +++ /dev/null @@ -1,77 +0,0 @@ -from sqlalchemy import Column, Integer, String, DateTime -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship -from sqlalchemy import ForeignKey - -from data.database import Session, engine - -Base = declarative_base() - - -class AnnouncementDbModel(Base): - __tablename__ = "announcements" - - id = Column(Integer, primary_key=True, autoincrement=True) - author = Column(String, nullable=True) - title = Column(String, nullable=False) - message = Column(String, nullable=True) - attachments = relationship("AttachmentDbModel", back_populates="announcements") - channel = Column(String, nullable=True) - created_at = Column(DateTime, nullable=False) - level = Column(Integer, nullable=True) - - -class AttachmentDbModel(Base): - __tablename__ = "attachments" - - id = Column(Integer, primary_key=True, autoincrement=True) - announcement_id = Column(Integer, ForeignKey("announcements.id")) - attachment_url = Column(String, nullable=False) - - announcements = relationship("AnnouncementDbModel", back_populates="attachments") - - -class UserDbModel(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True, autoincrement=True) - username = Column(String, nullable=False) - password = Column(String, nullable=False) - - # Required by sanic-beskar - @property - def rolenames(self): - return [] - - @classmethod - async def lookup(cls, username=None): - try: - session = Session() - - user = session.query(UserDbModel).filter_by(username=username).first() - - session.close() - - return user - except: - return None - - @classmethod - async def identify(cls, id): - try: - session = Session() - - user = session.query(UserDbModel).filter_by(id=id).first() - - session.close() - - return user - except: - return None - - @property - def identity(self): - return self.id - - -Base.metadata.create_all(engine) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 02d487c4..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3.8" - -services: - revanced-api: - container_name: revanced-api - image: ghcr.io/revanced/revanced-api:latest - volumes: - - /data/revanced-api:/usr/src/app/persistence - environment: - - GITHUB_TOKEN=YOUR_GITHUB_TOKEN - - SECRET_KEY=YOUR_SECRET_KEY - - USERNAME=YOUR_USERNAME - - PASSWORD=YOUR_PASSWORD - ports: - - 127.0.0.1:7934:8000 - restart: unless-stopped diff --git a/docs/.swagger-codegen-ignore b/docs/.swagger-codegen-ignore new file mode 100644 index 00000000..c5fa491b --- /dev/null +++ b/docs/.swagger-codegen-ignore @@ -0,0 +1,23 @@ +# Swagger Codegen Ignore +# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/docs/.swagger-codegen/VERSION b/docs/.swagger-codegen/VERSION new file mode 100644 index 00000000..a254f0ac --- /dev/null +++ b/docs/.swagger-codegen/VERSION @@ -0,0 +1 @@ +3.0.41 \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..af14f149 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,2752 @@ + + + + + Application API + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Application API

+
+
+
+ +
+
+

Default

+
+
+
+

rootGet

+

+
+
+
+

+

Hello World!

+

+
+
/
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET\
+-H "Accept: text/plain"\
+"http://0.0.0.0:8080/"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.DefaultApi;
+
+import java.io.File;
+import java.util.*;
+
+public class DefaultApiExample {
+
+    public static void main(String[] args) {
+        
+        DefaultApi apiInstance = new DefaultApi();
+        try {
+            'String' result = apiInstance.rootGet();
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling DefaultApi#rootGet");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.DefaultApi;
+
+public class DefaultApiExample {
+
+    public static void main(String[] args) {
+        DefaultApi apiInstance = new DefaultApi();
+        try {
+            'String' result = apiInstance.rootGet();
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling DefaultApi#rootGet");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+

+DefaultApi *apiInstance = [[DefaultApi alloc] init];
+
+[apiInstance rootGetWithCompletionHandler: 
+              ^('String' output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ApplicationApi = require('application_api');
+
+var api = new ApplicationApi.DefaultApi()
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.rootGet(callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class rootGetExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new DefaultApi();
+
+            try
+            {
+                'String' result = apiInstance.rootGet();
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling DefaultApi.rootGet: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiDefaultApi();
+
+try {
+    $result = $api_instance->rootGet();
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling DefaultApi->rootGet: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::DefaultApi;
+
+my $api_instance = WWW::SwaggerClient::DefaultApi->new();
+
+eval { 
+    my $result = $api_instance->rootGet();
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling DefaultApi->rootGet: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.DefaultApi()
+
+try: 
+    api_response = api_instance.root_get()
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling DefaultApi->rootGet: %s\n" % e)
+
+
+ +

Parameters

+ + + + + + +

Responses

+

Status: 200 - OK

+ + + +
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..859e808d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.parallel = true +org.gradle.caching = true +kotlin.code.style = official +version = 0.0.1 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ae96f6ab --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,41 @@ +[versions] +kotlin="1.9.22" +logback="1.4.14" +exposed="0.41.1" +h2="2.1.214" +koin="3.5.3" +dotenv="6.4.1" +ktor = "2.3.7" + +[libraries] +ktor-client-core = { module = "io.ktor:ktor-client-core" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" } +ktor-client-resources = { module = "io.ktor:ktor-client-resources" } +ktor-client-auth = { module = "io.ktor:ktor-client-auth" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation" } +ktor-server-conditional-headers = { module = "io.ktor:ktor-server-conditional-headers" } +ktor-server-core = { module = "io.ktor:ktor-server-core" } +ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" } +ktor-server-auth = { module = "io.ktor:ktor-server-auth" } +ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" } +ktor-server-swagger = { module = "io.ktor:ktor-server-swagger" } +ktor-server-openapi = { module = "io.ktor:ktor-server-openapi" } +ktor-server-cors = { module = "io.ktor:ktor-server-cors" } +ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } +ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } +ktor-server-netty = { module = "io.ktor:ktor-server-netty" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } +koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } +h2 = { module = "com.h2database:h2", version.ref = "h2" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" } +ktor-server-tests = { module = "io.ktor:ktor-server-tests" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } + +[plugins] +serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ktor = { id = "io.ktor.plugin", version.ref = "ktor" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e411586a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 9009c162..00000000 --- a/mypy.ini +++ /dev/null @@ -1,30 +0,0 @@ -[mypy] -python_version = 3.11 -pretty = true -follow_imports = normal -namespace_packages = true -show_column_numbers = true -show_error_codes = true -allow_redefinition = false -check_untyped_defs = true -implicit_reexport = false -strict_optional = true -strict_equality = true -warn_no_return = true -warn_redundant_casts = true -warn_unused_configs = true -warn_unused_ignores = true -warn_unreachable = true -plugins = pydantic.mypy - -[mypy-toolz.*] -ignore_missing_imports = True - -[mypy-sanic_testing.*] -ignore_missing_imports = True - -[mypy-fire.*] -ignore_missing_imports = True - -[mypy-cytoolz.*] -ignore_missing_imports = True diff --git a/persistence/.gitkeep b/persistence/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 590e15e5..00000000 --- a/poetry.lock +++ /dev/null @@ -1,2599 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[package]] -name = "aiodns" -version = "3.2.0" -description = "Simple DNS resolver for asyncio" -optional = false -python-versions = "*" -files = [ - {file = "aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5"}, - {file = "aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72"}, -] - -[package.dependencies] -pycares = ">=4.0.0" - -[[package]] -name = "aiofiles" -version = "23.2.1" -description = "File support for asyncio." -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, - {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, -] - -[[package]] -name = "aiohttp" -version = "3.9.3" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, - {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, - {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, - {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, - {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, - {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, - {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, - {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, - {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, - {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, - {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, - {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, -] - -[package.dependencies] -aiodns = {version = "*", optional = true, markers = "(sys_platform == \"linux\" or sys_platform == \"darwin\") and extra == \"speedups\""} -aiosignal = ">=1.1.2" -attrs = ">=17.3.0" -Brotli = {version = "*", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"speedups\""} -brotlicffi = {version = "*", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"speedups\""} -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "argon2-cffi" -version = "23.1.0" -description = "Argon2 for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[package.dependencies] -argon2-cffi-bindings = "*" - -[package.extras] -dev = ["argon2-cffi[tests,typing]", "tox (>4)"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] -tests = ["hypothesis", "pytest"] -typing = ["mypy"] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -description = "Low-level CFFI bindings for Argon2" -optional = false -python-versions = ">=3.6" -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, -] - -[package.dependencies] -cffi = ">=1.0.1" - -[package.extras] -dev = ["cogapp", "pre-commit", "pytest", "wheel"] -tests = ["pytest"] - -[[package]] -name = "asyncstdlib" -version = "3.12.2" -description = "The missing async toolbox" -optional = false -python-versions = "~=3.8" -files = [ - {file = "asyncstdlib-3.12.2-py3-none-any.whl", hash = "sha256:343b4ae330a76a8ed05fc83a03e2756dcaa7bede0cf4c7f0ba4aa7112feddec4"}, - {file = "asyncstdlib-3.12.2.tar.gz", hash = "sha256:3fea1eef246b68022c5bbaf6661db59f640ef54dd05fa1d0ac987e49b86a5529"}, -] - -[package.extras] -doc = ["sphinx", "sphinxcontrib-trio"] -test = ["black", "coverage", "flake8", "flake8-2020", "flake8-bugbear", "mypy", "pytest", "pytest-cov", "typing-extensions"] - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "brotli" -version = "1.1.0" -description = "Python bindings for the Brotli compression library" -optional = false -python-versions = "*" -files = [ - {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, - {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, - {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, - {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, - {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, - {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, - {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, - {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, - {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, - {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, - {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, - {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, - {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, - {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, - {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, - {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, - {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, - {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, - {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, - {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, - {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, - {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, - {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, - {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, - {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, - {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, - {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, - {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, - {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, - {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, - {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, - {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, - {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, - {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, - {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, -] - -[[package]] -name = "brotlicffi" -version = "1.1.0.0" -description = "Python CFFI bindings to the Brotli library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, - {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, - {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, - {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, - {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, - {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, - {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, -] - -[package.dependencies] -cffi = ">=1.0.0" - -[[package]] -name = "bson" -version = "0.5.10" -description = "BSON codec for Python" -optional = false -python-versions = "*" -files = [ - {file = "bson-0.5.10.tar.gz", hash = "sha256:d6511b2ab051139a9123c184de1a04227262173ad593429d21e443d6462d6590"}, -] - -[package.dependencies] -python-dateutil = ">=2.4.0" -six = ">=1.9.0" - -[[package]] -name = "certifi" -version = "2024.2.2" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, -] - -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cryptography" -version = "41.0.7" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, -] - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "cytoolz" -version = "0.12.3" -description = "Cython implementation of Toolz: High performance functional utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cytoolz-0.12.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbe58e26c84b163beba0fbeacf6b065feabc8f75c6d3fe305550d33f24a2d346"}, - {file = "cytoolz-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c51b66ada9bfdb88cf711bf350fcc46f82b83a4683cf2413e633c31a64df6201"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e70d9c615e5c9dc10d279d1e32e846085fe1fd6f08d623ddd059a92861f4e3dd"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83f4532707963ae1a5108e51fdfe1278cc8724e3301fee48b9e73e1316de64f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d028044524ee2e815f36210a793c414551b689d4f4eda28f8bbb0883ad78bf5f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c2875bcd1397d0627a09a4f9172fa513185ad302c63758efc15b8eb33cc2a98"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:131ff4820e5d64a25d7ad3c3556f2d8aa65c66b3f021b03f8a8e98e4180dd808"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04afa90d9d9d18394c40d9bed48c51433d08b57c042e0e50c8c0f9799735dcbd"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc1ca9c610425f9854323669a671fc163300b873731584e258975adf50931164"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa3f8e01bc423a933f2e1c510cbb0632c6787865b5242857cc955cae220d1bf"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f702e295dddef5f8af4a456db93f114539b8dc2a7a9bc4de7c7e41d169aa6ec3"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fbad1fb9bb47e827d00e01992a099b0ba79facf5e5aa453be066033232ac4b5"}, - {file = "cytoolz-0.12.3-cp310-cp310-win32.whl", hash = "sha256:8587c3c3dbe78af90c5025288766ac10dc2240c1e76eb0a93a4e244c265ccefd"}, - {file = "cytoolz-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e45803d9e75ef90a2f859ef8f7f77614730f4a8ce1b9244375734567299d239"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ac4f2fb38bbc67ff1875b7d2f0f162a247f43bd28eb7c9d15e6175a982e558d"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf1e1e96dd86829a0539baf514a9c8473a58fbb415f92401a68e8e52a34ecd5"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08a438701c6141dd34eaf92e9e9a1f66e23a22f7840ef8a371eba274477de85d"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b6f11b0d7ed91be53166aeef2a23a799e636625675bb30818f47f41ad31821"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fde09384d23048a7b4ac889063761e44b89a0b64015393e2d1d21d5c1f534a"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d3bfe45173cc8e6c76206be3a916d8bfd2214fb2965563e288088012f1dabfc"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27513a5d5b6624372d63313574381d3217a66e7a2626b056c695179623a5cb1a"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d294e5e81ff094fe920fd545052ff30838ea49f9e91227a55ecd9f3ca19774a0"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:727b01a2004ddb513496507a695e19b5c0cfebcdfcc68349d3efd92a1c297bf4"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:fe1e1779a39dbe83f13886d2b4b02f8c4b10755e3c8d9a89b630395f49f4f406"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:de74ef266e2679c3bf8b5fc20cee4fc0271ba13ae0d9097b1491c7a9bcadb389"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e04d22049233394e0b08193aca9737200b4a2afa28659d957327aa780ddddf2"}, - {file = "cytoolz-0.12.3-cp311-cp311-win32.whl", hash = "sha256:20d36430d8ac809186736fda735ee7d595b6242bdb35f69b598ef809ebfa5605"}, - {file = "cytoolz-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:780c06110f383344d537f48d9010d79fa4f75070d214fc47f389357dd4f010b6"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:86923d823bd19ce35805953b018d436f6b862edd6a7c8b747a13d52b39ed5716"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3e61acfd029bfb81c2c596249b508dfd2b4f72e31b7b53b62e5fb0507dd7293"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd728f4e6051af6af234651df49319da1d813f47894d4c3c8ab7455e01703a37"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe8c6267caa7ec67bcc37e360f0d8a26bc3bdce510b15b97f2f2e0143bdd3673"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99462abd8323c52204a2a0ce62454ce8fa0f4e94b9af397945c12830de73f27e"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da125221b1fa25c690fcd030a54344cecec80074df018d906fc6a99f46c1e3a6"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c18e351956f70db9e2d04ff02f28e9a41839250d3f936a4c8a1eabd1c3094d2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:921e6d2440ac758c4945c587b1d1d9b781b72737ac0c0ca5d5e02ca1db8bded2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1651a9bd591a8326329ce1d6336f3129161a36d7061a4d5ea9e5377e033364cf"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8893223b87c2782bd59f9c4bd5c7bf733edd8728b523c93efb91d7468b486528"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:e4d2961644153c5ae186db964aa9f6109da81b12df0f1d3494b4e5cf2c332ee2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:71b6eb97f6695f7ba8ce69c49b707a351c5f46fd97f5aeb5f6f2fb0d6e72b887"}, - {file = "cytoolz-0.12.3-cp312-cp312-win32.whl", hash = "sha256:cee3de65584e915053412cd178729ff510ad5f8f585c21c5890e91028283518f"}, - {file = "cytoolz-0.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:9eef0d23035fa4dcfa21e570961e86c375153a7ee605cdd11a8b088c24f707f6"}, - {file = "cytoolz-0.12.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9a38332cfad2a91e89405b7c18b3f00e2edc951c225accbc217597d3e4e9fde"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f501ae1353071fa5d6677437bbeb1aeb5622067dce0977cedc2c5ec5843b202"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56f899758146a52e2f8cfb3fb6f4ca19c1e5814178c3d584de35f9e4d7166d91"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800f0526adf9e53d3c6acda748f4def1f048adaa780752f154da5cf22aa488a2"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0976a3fcb81d065473173e9005848218ce03ddb2ec7d40dd6a8d2dba7f1c3ae"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c835eab01466cb67d0ce6290601ebef2d82d8d0d0a285ed0d6e46989e4a7a71a"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4fba0616fcd487e34b8beec1ad9911d192c62e758baa12fcb44448b9b6feae22"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f6e8207d732651e0204779e1ba5a4925c93081834570411f959b80681f8d333"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8119bf5961091cfe644784d0bae214e273b3b3a479f93ee3baab97bbd995ccfe"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7ad1331cb68afeec58469c31d944a2100cee14eac221553f0d5218ace1a0b25d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:92c53d508fb8a4463acc85b322fa24734efdc66933a5c8661bdc862103a3373d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:2c6dd75dae3d84fa8988861ab8b1189d2488cb8a9b8653828f9cd6126b5e7abd"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:caf07a97b5220e6334dd32c8b6d8b2bd255ca694eca5dfe914bb5b880ee66cdb"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed0cfb9326747759e2ad81cb6e45f20086a273b67ac3a4c00b19efcbab007c60"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:96a5a0292575c3697121f97cc605baf2fd125120c7dcdf39edd1a135798482ca"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b76f2f50a789c44d6fd7f773ec43d2a8686781cd52236da03f7f7d7998989bee"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2905fdccacc64b4beba37f95cab9d792289c80f4d70830b70de2fc66c007ec01"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ebe23028eac51251f22ba01dba6587d30aa9c320372ca0c14eeab67118ec3f"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c715404a3825e37fe3966fe84c5f8a1f036e7640b2a02dbed96cac0c933451"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac0adffc1b6b6a4c5f1fd1dd2161afb720bcc771a91016dc6bdba59af0a5d3"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:37441bf4a2a4e2e0fe9c3b0ea5e72db352f5cca03903977ffc42f6f6c5467be9"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f04037302049cb30033f7fa4e1d0e44afe35ed6bfcf9b380fc11f2a27d3ed697"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f37b60e66378e7a116931d7220f5352186abfcc950d64856038aa2c01944929c"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ec9be3e4b6f86ea8b294d34c990c99d2ba6c526ef1e8f46f1d52c263d4f32cd7"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e9199c9e3fbf380a92b8042c677eb9e7ed4bccb126de5e9c0d26f5888d96788"}, - {file = "cytoolz-0.12.3-cp38-cp38-win32.whl", hash = "sha256:18cd61e078bd6bffe088e40f1ed02001387c29174750abce79499d26fa57f5eb"}, - {file = "cytoolz-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:765b8381d4003ceb1a07896a854eee2c31ebc950a4ae17d1e7a17c2a8feb2a68"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4a52dd2a36b0a91f7aa50ca6c8509057acc481a24255f6cb07b15d339a34e0f"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:581f1ce479769fe7eeb9ae6d87eadb230df8c7c5fff32138162cdd99d7fb8fc3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f505d4c6eb79585c8ad0b9dc140ef30a138c880e4e3b40230d642690e36366"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59276021619b432a5c21c01cda8320b9cc7dbc40351ffc478b440bfccd5bbdd3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e44f4c25e1e7cf6149b499c74945a14649c8866d36371a2c2d2164e4649e7755"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c64f8e60c1dd69e4d5e615481f2d57937746f4a6be2d0f86e9e7e3b9e2243b5e"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c63186f3bf9d7ef1347bc0537bb9a0b4111a0d7d6e619623cabc18fef0dc3b"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fdddb9d988405f24035234f1e8d1653ab2e48cc2404226d21b49a129aefd1d25"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6986632d8a969ea1e720990c818dace1a24c11015fd7c59b9fea0b65ef71f726"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0ba1cbc4d9cd7571c917f88f4a069568e5121646eb5d82b2393b2cf84712cf2a"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7d267ffc9a36c0a9a58c7e0adc9fa82620f22e4a72533e15dd1361f57fc9accf"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95e878868a172a41fbf6c505a4b967309e6870e22adc7b1c3b19653d062711fa"}, - {file = "cytoolz-0.12.3-cp39-cp39-win32.whl", hash = "sha256:8e21932d6d260996f7109f2a40b2586070cb0a0cf1d65781e156326d5ebcc329"}, - {file = "cytoolz-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:0d8edfbc694af6c9bda4db56643fb8ed3d14e47bec358c2f1417de9a12d6d1fb"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f9bd1ae6c2a27eda5abe2a0b65a83029d2385c5a1da7b8ef47af5905d7e905"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2d271393c378282727f1231d40391ae93b93ddc0997448acc21dd0cb6a1e56d"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee98968d6a66ee83a8ceabf31182189ab5d8598998c8ce69b6d5843daeb2db60"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01cfb8518828c1189200c02a5010ea404407fb18fd5589e29c126e84bbeadd36"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:456395d7aec01db32bf9e6db191d667347c78d8d48e77234521fa1078f60dabb"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cd88028bb897fba99ddd84f253ca6bef73ecb7bdf3f3cf25bc493f8f97d3c7c5"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b19223e7f7bd7a73ec3aa6fdfb73b579ff09c2bc0b7d26857eec2d01a58c76"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a79d72b08048a0980a59457c239555f111ac0c8bdc140c91a025f124104dbb4"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd70141b32b717696a72b8876e86bc9c6f8eff995c1808e299db3541213ff82"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a1445c91009eb775d479e88954c51d0b4cf9a1e8ce3c503c2672d17252882647"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ca6a9a9300d5bda417d9090107c6d2b007683efc59d63cc09aca0e7930a08a85"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6feb903d2a08a4ba2e70e950e862fd3be9be9a588b7c38cee4728150a52918"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b6f43f086e5a965d33d62a145ae121b4ccb6e0789ac0acc895ce084fec8c65"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:534fa66db8564d9b13872d81d54b6b09ae592c585eb826aac235bd6f1830f8ad"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fea649f979def23150680de1bd1d09682da3b54932800a0f90f29fc2a6c98ba8"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a447247ed312dd64e3a8d9483841ecc5338ee26d6e6fbd29cd373ed030db0240"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba3f843aa89f35467b38c398ae5b980a824fdbdb94065adc6ec7c47a0a22f4c7"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:582c22f97a380211fb36a7b65b1beeb84ea11d82015fa84b054be78580390082"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47feb089506fc66e1593cd9ade3945693a9d089a445fbe9a11385cab200b9f22"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ba9002d2f043943744a9dc8e50a47362bcb6e6f360dc0a1abcb19642584d87bb"}, - {file = "cytoolz-0.12.3.tar.gz", hash = "sha256:4503dc59f4ced53a54643272c61dc305d1dbbfbd7d6bdf296948de9f34c3a282"}, -] - -[package.dependencies] -toolz = ">=0.8.0" - -[package.extras] -cython = ["cython"] - -[[package]] -name = "deprecated" -version = "1.2.14" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - -[[package]] -name = "fastpbkdf2" -version = "0.2" -description = "A faster implementation of PBKDF2" -optional = false -python-versions = "*" -files = [ - {file = "fastpbkdf2-0.2.tar.gz", hash = "sha256:88fe92e9c3b6972421ba744ff4792334e4855b2d37534417f8c98c7cdba5ea9a"}, -] - -[package.dependencies] -cffi = ">=1.1" -six = ">=1.4.1" - -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - -[[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "html5tagger" -version = "1.3.0" -description = "Pythonic HTML generation/templating (no template files)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "html5tagger-1.3.0-py3-none-any.whl", hash = "sha256:ce14313515edffec8ed8a36c5890d023922641171b4e6e5774ad1a74998f5351"}, - {file = "html5tagger-1.3.0.tar.gz", hash = "sha256:84fa3dfb49e5c83b79bbd856ab7b1de8e2311c3bb46a8be925f119e3880a8da9"}, -] - -[[package]] -name = "httptools" -version = "0.6.1" -description = "A collection of framework independent HTTP protocol utils." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, - {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, - {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, - {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, - {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, - {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, - {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, -] - -[package.extras] -test = ["Cython (>=0.29.24,<0.30.0)"] - -[[package]] -name = "idna" -version = "3.6" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, -] - -[[package]] -name = "importlib-resources" -version = "6.4.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] - -[[package]] -name = "iso8601" -version = "2.1.0" -description = "Simple module to parse ISO 8601 dates" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, - {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, -] - -[[package]] -name = "jinja2" -version = "3.1.3" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "limits" -version = "3.10.1" -description = "Rate limiting utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "limits-3.10.1-py3-none-any.whl", hash = "sha256:446242f5a6f7b8c7744e286a70793264ed81bca97860f94b821347284d14fbe9"}, - {file = "limits-3.10.1.tar.gz", hash = "sha256:1ee31d169d498da267a1b72183ae5940afc64b17b4ed4dfd977f6ea5607c2cfb"}, -] - -[package.dependencies] -deprecated = ">=1.2" -importlib-resources = ">=1.3" -packaging = ">=21,<25" -typing-extensions = "*" - -[package.extras] -all = ["aetcd", "coredis (>=3.4.0,<5)", "emcache (>=0.6.1)", "emcache (>=1)", "etcd3", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<6.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)"] -async-etcd = ["aetcd"] -async-memcached = ["emcache (>=0.6.1)", "emcache (>=1)"] -async-mongodb = ["motor (>=3,<4)"] -async-redis = ["coredis (>=3.4.0,<5)"] -etcd = ["etcd3"] -memcached = ["pymemcache (>3,<5.0.0)"] -mongodb = ["pymongo (>4.1,<5)"] -redis = ["redis (>3,!=4.5.2,!=4.5.3,<6.0.0)"] -rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] - -[[package]] -name = "lxml" -version = "5.2.1" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -files = [ - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"}, - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"}, - {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"}, - {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, - {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, - {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"}, - {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"}, - {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"}, - {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"}, - {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"}, - {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"}, - {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"}, - {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"}, - {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"}, - {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"}, - {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"}, - {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"}, - {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.10)"] - -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - -[[package]] -name = "mypy" -version = "1.9.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "packaging" -version = "24.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, -] - -[[package]] -name = "passlib" -version = "1.7.4" -description = "comprehensive password hashing framework supporting over 30 schemes" -optional = false -python-versions = "*" -files = [ - {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, - {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, -] - -[package.extras] -argon2 = ["argon2-cffi (>=18.2.0)"] -bcrypt = ["bcrypt (>=3.1.0)"] -build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] -totp = ["cryptography"] - -[[package]] -name = "pendulum" -version = "2.1.2" -description = "Python datetimes made easy" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"}, - {file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"}, - {file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"}, - {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"}, - {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"}, - {file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"}, - {file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"}, - {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"}, - {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"}, - {file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"}, - {file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"}, - {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"}, - {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"}, - {file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"}, - {file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"}, - {file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"}, - {file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"}, - {file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"}, - {file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"}, - {file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"}, - {file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"}, -] - -[package.dependencies] -python-dateutil = ">=2.6,<3.0" -pytzdata = ">=2020.1" - -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - -[[package]] -name = "py-buzz" -version = "4.1.0" -description = "\"That's not flying, it's falling with style\": Exceptions with extras" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "py_buzz-4.1.0-py3-none-any.whl", hash = "sha256:77dc0dc9c9923b6f8079dc2e2c3b4fbebd2308acaca1500f8eda2711cd308f97"}, - {file = "py_buzz-4.1.0.tar.gz", hash = "sha256:ac11dba4922b2af114126044597d2fcd15e8c6a74368bed91f3c6732c8f09812"}, -] - -[[package]] -name = "pycares" -version = "4.4.0" -description = "Python interface for c-ares" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, - {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9"}, - {file = "pycares-4.4.0-cp310-cp310-win32.whl", hash = "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0"}, - {file = "pycares-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c"}, - {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490"}, - {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f"}, - {file = "pycares-4.4.0-cp311-cp311-win32.whl", hash = "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f"}, - {file = "pycares-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502"}, - {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6"}, - {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4"}, - {file = "pycares-4.4.0-cp312-cp312-win32.whl", hash = "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0"}, - {file = "pycares-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977"}, - {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95"}, - {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4"}, - {file = "pycares-4.4.0-cp38-cp38-win32.whl", hash = "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f"}, - {file = "pycares-4.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51"}, - {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741"}, - {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b"}, - {file = "pycares-4.4.0-cp39-cp39-win32.whl", hash = "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47"}, - {file = "pycares-4.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2"}, - {file = "pycares-4.4.0.tar.gz", hash = "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2"}, -] - -[package.dependencies] -cffi = ">=1.5.0" - -[package.extras] -idna = ["idna (>=2.1)"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pycryptodomex" -version = "3.20.0" -description = "Cryptographic library for Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, - {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, -] - -[[package]] -name = "pydantic" -version = "1.10.15" -description = "Data validation and settings management using python type hints" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, - {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, - {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, - {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, - {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, - {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, - {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, - {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, - {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, - {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, -] - -[package.dependencies] -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pyjwt" -version = "2.8.0" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, - {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pyseto" -version = "1.7.7" -description = "A Python implementation of PASETO/PASERK." -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "pyseto-1.7.7-py3-none-any.whl", hash = "sha256:05dbb906a0b27bba22e28fc4289b7a7743b1d820fa2afaa79a25f6cef945014d"}, - {file = "pyseto-1.7.7.tar.gz", hash = "sha256:c0f1fe56c529f7001e93b6f646d4a47469ecee45df3fa749d467e8721f5b0f6e"}, -] - -[package.dependencies] -argon2-cffi = ">=23.1.0,<24.0.0" -cryptography = ">=41.0.5,<42" -iso8601 = ">=1.0.2,<3.0.0" -pycryptodomex = ">=3.18.0,<4.0.0" - -[package.extras] -docs = ["Sphinx[docs] (>=6,<8)", "sphinx-autodoc-typehints[docs] (>=1.21.0,<2.0.0)", "sphinx-rtd-theme[docs] (>=1.2.1,<2.0.0)"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytzdata" -version = "2020.1" -description = "The Olson timezone database for Python." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, - {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "sanic" -version = "23.12.1" -description = "A web server and web framework that's written to go fast. Build fast. Run fast." -optional = false -python-versions = ">=3.8" -files = [ - {file = "sanic-23.12.1-py3-none-any.whl", hash = "sha256:e292293b2663a7afeb380bdc48ab93978468b27deae46ad9561513941eb0311f"}, - {file = "sanic-23.12.1.tar.gz", hash = "sha256:2528ca81d2bdc58ea67d93c500df1a9c58404904b0bc3442425b464c72b4bb84"}, -] - -[package.dependencies] -aiofiles = ">=0.6.0" -html5tagger = ">=1.2.1" -httptools = ">=0.0.10" -multidict = ">=5.0,<7.0" -sanic-ext = {version = "*", optional = true, markers = "extra == \"ext\""} -sanic-routing = ">=23.12.0" -tracerite = ">=1.0.0" -typing-extensions = ">=4.4.0" -ujson = {version = ">=1.35", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} -uvloop = {version = ">=0.15.0", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} -websockets = ">=10.0" - -[package.extras] -all = ["autodocsumm (>=0.2.11)", "bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "cryptography", "docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "mypy", "pygments", "pytest (==7.1.*)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)", "towncrier", "tox", "types-ujson", "uvicorn (<0.15.0)"] -dev = ["bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "cryptography", "docutils", "mypy", "pygments", "pytest (==7.1.*)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "towncrier", "tox", "types-ujson", "uvicorn (<0.15.0)"] -docs = ["autodocsumm (>=0.2.11)", "docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "pygments", "sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)"] -ext = ["sanic-ext"] -http3 = ["aioquic"] -test = ["bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "docutils", "mypy", "pygments", "pytest (==7.1.*)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "types-ujson", "uvicorn (<0.15.0)"] - -[[package]] -name = "sanic-beskar" -version = "2.3.3.post1" -description = "Strong, Simple, (now async!) and Precise security for Sanic APIs" -optional = false -python-versions = ">=3.9,<4.0" -files = [ - {file = "sanic_beskar-2.3.3.post1-py3-none-any.whl", hash = "sha256:25b25fcc8dbd9d5214ada9fd613d6d41f02a76f001e919648b122bf4c6689bb1"}, - {file = "sanic_beskar-2.3.3.post1.tar.gz", hash = "sha256:31a5a09b943772555634eb627d97e3e374160fdcfe5dde57dabc4f5e3e9ea012"}, -] - -[package.dependencies] -cryptography = ">=39.0.0" -jinja2 = ">=3.1.2" -passlib = ">=1.7" -pendulum = [ - {version = ">=2.1,<3.0", markers = "python_version >= \"3.9\" and python_version < \"3.12\""}, - {version = "==3.0.*", markers = "python_version >= \"3.12\""}, -] -py-buzz = ">3.2.0" -pyjwt = ">=2.6" -pyseto = ">=1.6.9" -sanic = ">=22.6.0" - -[[package]] -name = "sanic-ext" -version = "23.12.0" -description = "Extend your Sanic installation with some core functionality." -optional = false -python-versions = "*" -files = [ - {file = "sanic-ext-23.12.0.tar.gz", hash = "sha256:42fc41e7fafa58f3b790f685f3dd8a8de281460b4169d0e91f4e11b8747f845c"}, - {file = "sanic_ext-23.12.0-py3-none-any.whl", hash = "sha256:3ba2c143d7c41d89b87a11c6214b9d9b52c3994ff8ce3a03792b54ec5627e2c3"}, -] - -[package.dependencies] -pyyaml = ">=3.0.0" - -[package.extras] -dev = ["Jinja2", "black (>=21.4b2)", "coverage", "flake8 (>=3.7.7)", "isort (>=5.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "sanic-testing (>=22.9.0)", "tox"] -test = ["Jinja2", "coverage", "pytest", "pytest-asyncio", "pytest-cov", "sanic-testing (>=22.9.0)", "tox"] - -[[package]] -name = "sanic_limiter" -version = "0.1.4" -description = "Provides rate limiting features for Sanic. Supports in-memory, redis and memcache as storage." -optional = false -python-versions = "*" -files = [] -develop = false - -[package.dependencies] -limits = ">=1.2.1" -sanic = ">=0.4.0" -six = ">=1.4.1" - -[package.source] -type = "git" -url = "https://github.com/Omegastick/sanic-limiter" -reference = "HEAD" -resolved_reference = "843e13144aa21d843ce212a7c1db31b72ce8a103" - -[[package]] -name = "sanic-routing" -version = "23.12.0" -description = "Core routing component for Sanic" -optional = false -python-versions = "*" -files = [ - {file = "sanic-routing-23.12.0.tar.gz", hash = "sha256:1dcadc62c443e48c852392dba03603f9862b6197fc4cba5bbefeb1ace0848b04"}, - {file = "sanic_routing-23.12.0-py3-none-any.whl", hash = "sha256:1558a72afcb9046ed3134a5edae02fc1552cff08f0fff2e8d5de0877ea43ed73"}, -] - -[[package]] -name = "sentry-sdk" -version = "1.44.1" -description = "Python client for Sentry (https://sentry.io)" -optional = false -python-versions = "*" -files = [ - {file = "sentry-sdk-1.44.1.tar.gz", hash = "sha256:24e6a53eeabffd2f95d952aa35ca52f0f4201d17f820ac9d3ff7244c665aaf68"}, - {file = "sentry_sdk-1.44.1-py2.py3-none-any.whl", hash = "sha256:5f75eb91d8ab6037c754a87b8501cc581b2827e923682f593bed3539ce5b3999"}, -] - -[package.dependencies] -certifi = "*" -sanic = {version = ">=0.8", optional = true, markers = "extra == \"sanic\""} -urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} - -[package.extras] -aiohttp = ["aiohttp (>=3.5)"] -arq = ["arq (>=0.23)"] -asyncpg = ["asyncpg (>=0.23)"] -beam = ["apache-beam (>=2.12)"] -bottle = ["bottle (>=0.12.13)"] -celery = ["celery (>=3)"] -celery-redbeat = ["celery-redbeat (>=2)"] -chalice = ["chalice (>=1.16.0)"] -clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] -django = ["django (>=1.8)"] -falcon = ["falcon (>=1.4)"] -fastapi = ["fastapi (>=0.79.0)"] -flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] -grpcio = ["grpcio (>=1.21.1)"] -httpx = ["httpx (>=0.16.0)"] -huey = ["huey (>=2)"] -loguru = ["loguru (>=0.5)"] -openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] -opentelemetry = ["opentelemetry-distro (>=0.35b0)"] -opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] -pure-eval = ["asttokens", "executing", "pure-eval"] -pymongo = ["pymongo (>=3.1)"] -pyspark = ["pyspark (>=2.4.4)"] -quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] -rq = ["rq (>=0.6)"] -sanic = ["sanic (>=0.8)"] -sqlalchemy = ["sqlalchemy (>=1.2)"] -starlette = ["starlette (>=0.19.1)"] -starlite = ["starlite (>=1.48)"] -tornado = ["tornado (>=5)"] - -[[package]] -name = "setuptools" -version = "69.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.29" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, - {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, - {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "toolz" -version = "0.12.1" -description = "List processing tools and functional utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, -] - -[[package]] -name = "tracerite" -version = "1.1.1" -description = "Human-readable HTML tracebacks for Python exceptions" -optional = false -python-versions = "*" -files = [ - {file = "tracerite-1.1.1-py3-none-any.whl", hash = "sha256:3a787a9ecb1a136ea9ce17e6328e414ec414a4f644130af4e1e330bec2dece29"}, - {file = "tracerite-1.1.1.tar.gz", hash = "sha256:6400a35a187747189e4bb8d4a8e471bd86d14dbdcc94bcad23f4eda023f41356"}, -] - -[package.dependencies] -html5tagger = ">=1.2.1" - -[[package]] -name = "types-aiofiles" -version = "23.2.0.20240403" -description = "Typing stubs for aiofiles" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-aiofiles-23.2.0.20240403.tar.gz", hash = "sha256:1ffcf8f5f72b81f71139f754ea2610ab0017f27ba4fd771e187b07840ee49c0f"}, - {file = "types_aiofiles-23.2.0.20240403-py3-none-any.whl", hash = "sha256:adeeb4b999f19fda2dfe91c07857ff54701b6ee9b227b523a5a7be92125a2c5f"}, -] - -[[package]] -name = "types-beautifulsoup4" -version = "4.12.0.20240229" -description = "Typing stubs for beautifulsoup4" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-beautifulsoup4-4.12.0.20240229.tar.gz", hash = "sha256:e37e4cfa11b03b01775732e56d2c010cb24ee107786277bae6bc0fa3e305b686"}, - {file = "types_beautifulsoup4-4.12.0.20240229-py3-none-any.whl", hash = "sha256:000cdddb8aee4effb45a04be95654de8629fb8594a4f2f1231cff81108977324"}, -] - -[package.dependencies] -types-html5lib = "*" - -[[package]] -name = "types-html5lib" -version = "1.1.11.20240228" -description = "Typing stubs for html5lib" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-html5lib-1.1.11.20240228.tar.gz", hash = "sha256:22736b7299e605ec4ba539d48691e905fd0c61c3ea610acc59922232dc84cede"}, - {file = "types_html5lib-1.1.11.20240228-py3-none-any.whl", hash = "sha256:af5de0125cb0fe5667543b158db83849b22e25c0e36c9149836b095548bf1020"}, -] - -[[package]] -name = "types-ujson" -version = "5.9.0.0" -description = "Typing stubs for ujson" -optional = false -python-versions = ">=3.7" -files = [ - {file = "types-ujson-5.9.0.0.tar.gz", hash = "sha256:7e7042454dc7cd7f31b09c420d7caf36b93d30bdf4b8db93791bd0561713d017"}, - {file = "types_ujson-5.9.0.0-py3-none-any.whl", hash = "sha256:f274fa604ed6317effcd1c424ef4cf292c3b0689cb118fb3180689d40ed1f4ed"}, -] - -[[package]] -name = "typing-extensions" -version = "4.11.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, -] - -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "ujson" -version = "5.9.0" -description = "Ultra fast JSON encoder and decoder for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"}, - {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"}, - {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"}, - {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"}, - {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"}, - {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"}, - {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"}, - {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"}, - {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"}, - {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"}, - {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"}, - {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"}, - {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, -] - -[[package]] -name = "urllib3" -version = "2.2.1" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvloop" -version = "0.19.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, - {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, -] - -[package.extras] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "websockets" -version = "12.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, -] - -[[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - -[metadata] -lock-version = "2.0" -python-versions = ">=3.11,<3.13" -content-hash = "674baa9bbe0b000407c2864d512462b9fa77f42ed81da2fea0fc8e8b944e403e" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index a6261c8e..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,45 +0,0 @@ -[tool.poetry] -name = "revanced-api" -version = "0.1.0" -description = "" -authors = ["Alexandre Teles "] -license = "AGPLv3" -readme = "README.md" - -[tool.poetry.dependencies] -python = ">=3.11,<3.13" -aiohttp = { version = "^3.9.1", extras = ["speedups"] } -sanic = { version = "^23.12.1", extras = ["ext"] } -ujson = "^5.9.0" -pydantic = "^1.10.13" -asyncstdlib = "^3.12.0" -cytoolz = "^0.12.2" -beautifulsoup4 = "^4.12.2" -lxml = "^5.1.0" -sqlalchemy = "^2.0.25" -sanic-beskar = "^2.3.2" -bson = "^0.5.10" -fastpbkdf2 = "^0.2" -cryptography = "^41.0.7" -sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" } -sentry-sdk = { extras = ["sanic"], version = "^1.39.2" } - -[tool.poetry.dev-dependencies] -mypy = "^1.8.0" -types-ujson = "^5.9.0.0" -types-aiofiles = "^23.2.0.20240106" -types-beautifulsoup4 = "^4.12.0.20240106" - -[tool.poetry.group.dev.dependencies] -setuptools = "^69.2.0" - -[tool.pytest.ini_options] -asyncio_mode = "auto" -filterwarnings = [ - "ignore::DeprecationWarning", - "ignore::pytest.PytestCollectionWarning", -] - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 62ff52d0..00000000 --- a/requirements.txt +++ /dev/null @@ -1,62 +0,0 @@ -aiodns==3.1.1 ; (sys_platform == "linux" or sys_platform == "darwin") and python_version >= "3.11" and python_version < "3.13" -aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "3.13" -aiohttp[speedups]==3.9.1 ; python_version >= "3.11" and python_version < "3.13" -aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "3.13" -argon2-cffi==23.1.0 ; python_version >= "3.11" and python_version < "3.13" -argon2-cffi-bindings==21.2.0 ; python_version >= "3.11" and python_version < "3.13" -asyncstdlib==3.12.0 ; python_version >= "3.11" and python_version < "3.13" -attrs==23.2.0 ; python_version >= "3.11" and python_version < "3.13" -beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "3.13" -brotli==1.1.0 ; platform_python_implementation == "CPython" and python_version >= "3.11" and python_version < "3.13" -brotlicffi==1.1.0.0 ; platform_python_implementation != "CPython" and python_version >= "3.11" and python_version < "3.13" -bson==0.5.10 ; python_version >= "3.11" and python_version < "3.13" -certifi==2023.11.17 ; python_version >= "3.11" and python_version < "3.13" -cffi==1.16.0 ; python_version >= "3.11" and python_version < "3.13" -cryptography==41.0.7 ; python_version >= "3.11" and python_version < "3.13" -cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "3.13" -deprecated==1.2.14 ; python_version >= "3.11" and python_version < "3.13" -fastpbkdf2==0.2 ; python_version >= "3.11" and python_version < "3.13" -frozenlist==1.4.1 ; python_version >= "3.11" and python_version < "3.13" -greenlet==3.0.3 ; python_version >= "3.11" and python_version < "3.13" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") -html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "3.13" -httptools==0.6.1 ; python_version >= "3.11" and python_version < "3.13" -idna==3.6 ; python_version >= "3.11" and python_version < "3.13" -importlib-resources==6.1.1 ; python_version >= "3.11" and python_version < "3.13" -iso8601==2.1.0 ; python_version >= "3.11" and python_version < "3.13" -jinja2==3.1.3 ; python_version >= "3.11" and python_version < "3.13" -limits==3.7.0 ; python_version >= "3.11" and python_version < "3.13" -lxml==5.1.0 ; python_version >= "3.11" and python_version < "3.13" -markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "3.13" -multidict==6.0.5 ; python_version >= "3.11" and python_version < "3.13" -packaging==23.2 ; python_version >= "3.11" and python_version < "3.13" -passlib==1.7.4 ; python_version >= "3.11" and python_version < "3.13" -pendulum==3.0.0 ; python_version >= "3.11" and python_version < "3.13" -py-buzz==4.1.0 ; python_version >= "3.11" and python_version < "3.13" -pycares==4.4.0 ; (sys_platform == "linux" or sys_platform == "darwin") and python_version >= "3.11" and python_version < "3.13" -pycparser==2.21 ; python_version >= "3.11" and python_version < "3.13" -pycryptodomex==3.20.0 ; python_version >= "3.11" and python_version < "3.13" -pydantic==1.10.13 ; python_version >= "3.11" and python_version < "3.13" -pyjwt==2.8.0 ; python_version >= "3.11" and python_version < "3.13" -pyseto==1.7.7 ; python_version >= "3.11" and python_version < "3.13" -python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "3.13" -pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "3.13" -sanic==23.12.1 ; python_version >= "3.11" and python_version < "3.13" -sanic-beskar==2.3.2 ; python_version >= "3.11" and python_version < "3.13" -sanic-ext==23.12.0 ; python_version >= "3.11" and python_version < "3.13" -sanic-limiter @ git+https://github.com/Omegastick/sanic-limiter@843e13144aa21d843ce212a7c1db31b72ce8a103 ; python_version >= "3.11" and python_version < "3.13" -sanic-routing==23.12.0 ; python_version >= "3.11" and python_version < "3.13" -sanic[ext]==23.12.1 ; python_version >= "3.11" and python_version < "3.13" -sentry-sdk[sanic]==1.39.2 ; python_version >= "3.11" and python_version < "3.13" -six==1.16.0 ; python_version >= "3.11" and python_version < "3.13" -soupsieve==2.5 ; python_version >= "3.11" and python_version < "3.13" -sqlalchemy==2.0.25 ; python_version >= "3.11" and python_version < "3.13" -toolz==0.12.0 ; python_version >= "3.11" and python_version < "3.13" -tracerite==1.1.1 ; python_version >= "3.11" and python_version < "3.13" -typing-extensions==4.9.0 ; python_version >= "3.11" and python_version < "3.13" -tzdata==2023.4 ; python_version >= "3.11" and python_version < "3.13" -ujson==5.9.0 ; python_version >= "3.11" and python_version < "3.13" -urllib3==2.1.0 ; python_version >= "3.11" and python_version < "3.13" -uvloop==0.19.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "3.13" -websockets==12.0 ; python_version >= "3.11" and python_version < "3.13" -wrapt==1.16.0 ; python_version >= "3.11" and python_version < "3.13" -yarl==1.9.4 ; python_version >= "3.11" and python_version < "3.13" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..37eb408b --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,7 @@ +rootProject.name = "app.revanced.revanced-api" + +buildCache { + local { + isEnabled = "CI" !in System.getenv() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/Application.kt b/src/main/kotlin/app/revanced/api/Application.kt new file mode 100644 index 00000000..d7dd38b4 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/Application.kt @@ -0,0 +1,24 @@ +package app.revanced.api + +import app.revanced.api.plugins.* +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* + +fun main() { + Dotenv.load() + + embeddedServer(Netty, port = 8080, host = "0.0.0.0", configure = { + connectionGroupSize = 1 + workerGroupSize = 1 + callGroupSize = 1 + }) { + configureHTTP() + configureSerialization() + configureDatabases() + configureSecurity() + configureDependencies() + configureRouting() + }.start(wait = true) +} diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/backend/Backend.kt new file mode 100644 index 00000000..efb1d02b --- /dev/null +++ b/src/main/kotlin/app/revanced/api/backend/Backend.kt @@ -0,0 +1,140 @@ +package app.revanced.api.backend + +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import kotlinx.serialization.Serializable + +/** + * The backend of the application used to get data for the API. + * + * @param httpClientConfig The configuration of the HTTP client. + */ +abstract class Backend( + httpClientConfig: HttpClientConfig.() -> Unit = {} +) { + protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) + + /** + * A user. + * + * @property name The name of the user. + * @property avatarUrl The URL to the avatar of the user. + * @property profileUrl The URL to the profile of the user. + */ + interface User { + val name: String + val avatarUrl: String + val profileUrl: String + } + + /** + * An organization. + * + * @property members The members of the organization. + */ + class Organization( + val members: Set + ) { + /** + * A member of an organization. + * + * @property name The name of the member. + * @property avatarUrl The URL to the avatar of the member. + * @property profileUrl The URL to the profile of the member. + * @property bio The bio of the member. + * @property gpgKeysUrl The URL to the GPG keys of the member. + */ + @Serializable + class Member ( + override val name: String, + override val avatarUrl: String, + override val profileUrl: String, + val bio: String?, + val gpgKeysUrl: String? + ) : User + + /** + * A repository of an organization. + * + * @property contributors The contributors of the repository. + */ + class Repository( + val contributors: Set + ) { + /** + * A contributor of a repository. + * + * @property name The name of the contributor. + * @property avatarUrl The URL to the avatar of the contributor. + * @property profileUrl The URL to the profile of the contributor. + */ + @Serializable + class Contributor( + override val name: String, + override val avatarUrl: String, + override val profileUrl: String + ) : User + + /** + * A release of a repository. + * + * @property tag The tag of the release. + * @property assets The assets of the release. + * @property createdAt The date and time the release was created. + * @property releaseNote The release note of the release. + */ + @Serializable + class Release( + val tag: String, + val releaseNote: String, + val createdAt: String, + val assets: Set + ) { + /** + * An asset of a release. + * + * @property downloadUrl The URL to download the asset. + */ + @Serializable + class Asset( + val downloadUrl: String + ) + } + } + } + + /** + * Get a release of a repository. + * + * @param owner The owner of the repository. + * @param repository The name of the repository. + * @param tag The tag of the release. If null, the latest release is returned. + * @param preRelease Whether to return a pre-release. + * If no pre-release exists, the latest release is returned. + * If tag is not null, this parameter is ignored. + * @return The release. + */ + abstract suspend fun getRelease( + owner: String, + repository: String, + tag: String? = null, + preRelease: Boolean = false + ): Organization.Repository.Release + + /** + * Get the contributors of a repository. + * + * @param owner The owner of the repository. + * @param repository The name of the repository. + * @return The contributors. + */ + abstract suspend fun getContributors(owner: String, repository: String): Set + + /** + * Get the members of an organization. + * + * @param organization The name of the organization. + * @return The members. + */ + abstract suspend fun getMembers(organization: String): Set +} diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt new file mode 100644 index 00000000..db144f50 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -0,0 +1,116 @@ +package app.revanced.api.backend.github + +import app.revanced.api.backend.Backend +import app.revanced.api.backend.github.api.Request +import io.ktor.client.call.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.resources.* +import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases +import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors +import app.revanced.api.backend.github.api.Request.Organization.Members +import app.revanced.api.backend.github.api.Response +import app.revanced.api.backend.github.api.Response.Organization.Repository.Release +import app.revanced.api.backend.github.api.Response.Organization.Repository.Contributor +import app.revanced.api.backend.github.api.Response.Organization.Member +import io.ktor.client.plugins.resources.Resources +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy + +@OptIn(ExperimentalSerializationApi::class) +class GitHubBackend(token: String? = null) : Backend({ + install(HttpCache) + install(Resources) + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase + }) + } + + defaultRequest { url("https://api.github.com") } + + token?.let { + install(Auth) { + bearer { + loadTokens { + BearerTokens( + accessToken = it, + refreshToken = "" // Required dummy value + ) + } + + sendWithoutRequest { true } + } + } + } +}) { + override suspend fun getRelease( + owner: String, + repository: String, + tag: String?, + preRelease: Boolean + ): Organization.Repository.Release { + val release = if (preRelease) { + val releases: Set = client.get(Releases(owner, repository)).body() + releases.firstOrNull { it.preReleases } ?: releases.first() // Latest pre-release or latest release + } else { + client.get( + tag?.let { Releases.Tag(owner, repository, it) } + ?: Releases.Latest(owner, repository) + ).body() + } + + return Organization.Repository.Release( + tag = release.tagName, + releaseNote = release.body, + createdAt = release.createdAt, + assets = release.assets.map { + Organization.Repository.Release.Asset( + downloadUrl = it.browserDownloadUrl + ) + }.toSet() + ) + } + + override suspend fun getContributors(owner: String, repository: String): Set { + val contributors: Set = client.get(Contributors(owner, repository)).body() + + return contributors.map { + Organization.Repository.Contributor( + name = it.login, + avatarUrl = it.avatarUrl, + profileUrl = it.url + ) + }.toSet() + } + + override suspend fun getMembers(organization: String): Set { + // Get the list of members of the organization. + val members: Set = client.get(Members(organization)).body>() + + return runBlocking(Dispatchers.Default) { + members.map { member -> + // Map the member to a user in order to get the bio. + async { + client.get(Request.User(member.login)).body() + } + } + }.awaitAll().map { user -> + // Map the user back to a member. + Organization.Member( + name = user.login, + avatarUrl = user.avatarUrl, + profileUrl = user.url, + bio = user.bio, + gpgKeysUrl = "https://github.com/${user.login}.gpg", + ) + }.toSet() + } +} diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt b/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt new file mode 100644 index 00000000..ab78199a --- /dev/null +++ b/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt @@ -0,0 +1,26 @@ +package app.revanced.api.backend.github.api + +import io.ktor.resources.* + +class Request { + @Resource("/users/{username}") + class User(val username: String) + class Organization { + @Resource("/orgs/{org}/members") + class Members(val org: String) + + class Repository { + @Resource("/repos/{owner}/{repo}/contributors") + class Contributors(val owner: String, val repo: String) + + @Resource("/repos/{owner}/{repo}/releases") + class Releases(val owner: String, val repo: String) { + @Resource("/repos/{owner}/{repo}/releases/tags/{tag}") + class Tag(val owner: String, val repo: String, val tag: String) + + @Resource("/repos/{owner}/{repo}/releases/latest") + class Latest(val owner: String, val repo: String) + } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt new file mode 100644 index 00000000..e286b957 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt @@ -0,0 +1,52 @@ +package app.revanced.api.backend.github.api + +import kotlinx.serialization.Serializable + + +class Response { + interface IUser { + val login: String + val avatarUrl: String + val url: String + } + + @Serializable + class User ( + override val login: String, + override val avatarUrl: String, + override val url: String, + val bio: String?, + ) : IUser + + class Organization { + @Serializable + class Member( + override val login: String, + override val avatarUrl: String, + override val url: String, + ) : IUser + + class Repository { + @Serializable + class Contributor( + override val login: String, + override val avatarUrl: String, + override val url: String, + ) : IUser + + @Serializable + class Release( + val tagName: String, + val assets: Set, + val preReleases: Boolean, + val createdAt: String, + val body: String + ) { + @Serializable + class Asset( + val browserDownloadUrl: String + ) + } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/Databases.kt b/src/main/kotlin/app/revanced/api/plugins/Databases.kt new file mode 100644 index 00000000..c023db16 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/Databases.kt @@ -0,0 +1,49 @@ +package app.revanced.api.plugins + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.jetbrains.exposed.sql.* + +fun Application.configureDatabases() { + val database = Database.connect( + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", + user = "root", + driver = "org.h2.Driver", + password = "" + ) + val userService = UserService(database) + routing { + // Create user + post("/users") { + val user = call.receive() + val id = userService.create(user) + call.respond(HttpStatusCode.Created, id) + } + // Read user + get("/users/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + val user = userService.read(id) + if (user != null) { + call.respond(HttpStatusCode.OK, user) + } else { + call.respond(HttpStatusCode.NotFound) + } + } + // Update user + put("/users/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + val user = call.receive() + userService.update(id, user) + call.respond(HttpStatusCode.OK) + } + // Delete user + delete("/users/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + userService.delete(id) + call.respond(HttpStatusCode.OK) + } + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt new file mode 100644 index 00000000..3950ec9d --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt @@ -0,0 +1,21 @@ +package app.revanced.api.plugins + +import app.revanced.api.backend.github.GitHubBackend +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.server.application.* +import org.koin.core.context.startKoin +import org.koin.dsl.module +import org.koin.ktor.ext.inject +import org.koin.ktor.plugin.Koin + +fun Application.configureDependencies() { + + install(Koin) { + modules( + module { + single { Dotenv.load() } + single { GitHubBackend(get().get("GITHUB_TOKEN")) } + } + ) + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt b/src/main/kotlin/app/revanced/api/plugins/HTTP.kt new file mode 100644 index 00000000..8ea4b73f --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/HTTP.kt @@ -0,0 +1,37 @@ +package app.revanced.api.plugins + +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.application.* +import io.ktor.server.plugins.cachingheaders.* +import io.ktor.server.plugins.conditionalheaders.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.openapi.* +import io.ktor.server.plugins.swagger.* +import io.ktor.server.routing.* + +fun Application.configureHTTP() { + install(ConditionalHeaders) + routing { + swaggerUI(path = "openapi") + } + routing { + openAPI(path = "openapi") + } + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + } + install(CachingHeaders) { + options { _, outgoingContent -> + when (outgoingContent.contentType?.withoutParameters()) { + ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60)) + else -> null + } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt new file mode 100644 index 00000000..c14a7c38 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/Routing.kt @@ -0,0 +1,45 @@ +package app.revanced.api.plugins + +import app.revanced.api.backend.github.GitHubBackend +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.inject + +fun Application.configureRouting() { + val backend by inject() + val dotenv by inject() + + routing { + route("/v${dotenv.get("API_VERSION", "1")}") { + route("/manager") { + get("/contributors") { + val contributors = backend.getContributors("revanced", "revanced-patches") + + call.respond(contributors) + } + + get("/members") { + val members = backend.getMembers("revanced") + + call.respond(members) + } + } + + route("/patches") { + + } + + route("/ping") { + handle { + call.respond(HttpStatusCode.NoContent) + } + } + } + + staticResources("/", "static") + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/Security.kt b/src/main/kotlin/app/revanced/api/plugins/Security.kt new file mode 100644 index 00000000..38bcf7f5 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/Security.kt @@ -0,0 +1,30 @@ +package app.revanced.api.plugins + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* + +fun Application.configureSecurity() { + // Please read the jwt property from the config file if you are using EngineMain + val jwtAudience = "jwt-audience" + val jwtDomain = "https://jwt-provider-domain/" + val jwtRealm = "ktor sample app" + val jwtSecret = "secret" + authentication { + jwt { + realm = jwtRealm + verifier( + JWT + .require(Algorithm.HMAC256(jwtSecret)) + .withAudience(jwtAudience) + .withIssuer(jwtDomain) + .build() + ) + validate { credential -> + if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null + } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/Serialization.kt b/src/main/kotlin/app/revanced/api/plugins/Serialization.kt new file mode 100644 index 00000000..e2cb0d18 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/Serialization.kt @@ -0,0 +1,11 @@ +package app.revanced.api.plugins + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt b/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt new file mode 100644 index 00000000..98679974 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt @@ -0,0 +1,59 @@ +package app.revanced.api.plugins + +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import kotlinx.serialization.Serializable +import kotlinx.coroutines.Dispatchers +import org.jetbrains.exposed.sql.* + +@Serializable +data class ExposedUser(val name: String, val age: Int) +class UserService(private val database: Database) { + object Users : Table() { + val id = integer("id").autoIncrement() + val name = varchar("name", length = 50) + val age = integer("age") + + override val primaryKey = PrimaryKey(id) + } + + init { + transaction(database) { + SchemaUtils.create(Users) + } + } + + suspend fun dbQuery(block: suspend () -> T): T = + newSuspendedTransaction(Dispatchers.IO) { block() } + + suspend fun create(user: ExposedUser): Int = dbQuery { + Users.insert { + it[name] = user.name + it[age] = user.age + }[Users.id] + } + + suspend fun read(id: Int): ExposedUser? { + return dbQuery { + Users.select { Users.id eq id } + .map { ExposedUser(it[Users.name], it[Users.age]) } + .singleOrNull() + } + } + + suspend fun update(id: Int, user: ExposedUser) { + dbQuery { + Users.update({ Users.id eq id }) { + it[name] = user.name + it[age] = user.age + } + } + } + + suspend fun delete(id: Int) { + dbQuery { + Users.deleteWhere { Users.id.eq(id) } + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000..3e11d781 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml new file mode 100644 index 00000000..2e6c6d76 --- /dev/null +++ b/src/main/resources/openapi/documentation.yaml @@ -0,0 +1,23 @@ +openapi: "3.0.3" +info: + title: "Application API" + description: "Application API" + version: "1.0.0" +servers: + - url: "http://0.0.0.0:8080" +paths: + /: + get: + description: "Hello World!" + responses: + "200": + description: "OK" + content: + text/plain: + schema: + type: "string" + examples: + Example#1: + value: "Hello World!" +components: + schemas: diff --git a/src/main/resources/static/about.json b/src/main/resources/static/about.json new file mode 100644 index 00000000..8f62d263 --- /dev/null +++ b/src/main/resources/static/about.json @@ -0,0 +1,99 @@ +{ + "name": "ReVanced", + "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", + "branding": + { + "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" + }, + "contact": + { + "email": "contact@revanced.app" + }, + "socials": + [ + { + "name": "Website", + "url": "https://revanced.app", + "preferred": true + }, + { + "name": "GitHub", + "url": "https://github.com/revanced", + "preferred": false + }, + { + "name": "Twitter", + "url": "https://twitter.com/revancedapp", + "preferred": false + }, + { + "name": "Discord", + "url": "https://revanced.app/discord", + "preferred": true + }, + { + "name": "Reddit", + "url": "https://www.reddit.com/r/revancedapp", + "preferred": false + }, + { + "name": "Telegram", + "url": "https://t.me/app_revanced", + "preferred": false + }, + { + "name": "YouTube", + "url": "https://www.youtube.com/@ReVanced", + "preferred": false + } + ], + "donations": + { + "wallets": + [ + { + "network": "Bitcoin", + "currency_code": "BTC", + "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f", + "preferred": false + }, + { + "network": "Dogecoin", + "currency_code": "DOGE", + "address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp", + "preferred": true + }, + { + "network": "Ethereum", + "currency_code": "ETH", + "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5", + "preferred": false + }, + { + "network": "Litecoin", + "currency_code": "LTC", + "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2", + "preferred": false + }, + { + "network": "Monero", + "currency_code": "XMR", + "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh", + "preferred": false + } + ], + "links": + [ + { + "name": "Open Collective", + "url": "https://opencollective.com/revanced", + "preferred": true + }, + { + "name": "GitHub Sponsors", + "url": "https://github.com/sponsors/ReVanced", + "preferred": false + } + ] + } +} \ No newline at end of file diff --git a/src/main/resources/static/robots.txt b/src/main/resources/static/robots.txt new file mode 100644 index 00000000..77470cb3 --- /dev/null +++ b/src/main/resources/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt new file mode 100644 index 00000000..c4ba45bc --- /dev/null +++ b/src/test/kotlin/app/revanced/ApplicationTest.kt @@ -0,0 +1,21 @@ +package app.revanced + +import app.revanced.api.plugins.configureRouting +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlin.test.* + +class ApplicationTest { + @Test + fun testRoot() = testApplication { + application { + configureRouting() + } + client.get("/").apply { + assertEquals(HttpStatusCode.OK, status) + assertEquals("Hello World!", bodyAsText()) + } + } +} diff --git a/static/img/favicon.ico b/static/img/favicon.ico deleted file mode 100644 index 0a4ba62047862221c80b95a793c80a4acb67a6c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20239 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 JPDHLkV1l{h5JLa} From 9999b242ad05dcea9e6b021e3ed4eeead4567805 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 03:18:31 +0100 Subject: [PATCH 02/81] feat: Implement more routes and add configuration --- .env.example | 2 +- .gitignore | 3 +- build.gradle.kts | 4 ++ configuration.example.toml | 5 ++ configuration.toml | 14 +++++ gradle/libs.versions.toml | 3 + src/main/kotlin/app/revanced/api/APISchema.kt | 55 ++++++++++++++++ .../kotlin/app/revanced/api/Application.kt | 2 - .../app/revanced/api/ConfigurationSchema.kt | 17 +++++ .../app/revanced/api/backend/Backend.kt | 50 +++++++-------- .../api/backend/github/GitHubBackend.kt | 53 ++++++++-------- .../api/backend/github/api/ResponseSchema.kt | 27 ++++---- .../app/revanced/api/plugins/Dependencies.kt | 20 +++++- .../app/revanced/api/plugins/Routing.kt | 63 +++++++++++++++---- .../static/{about.json => api/about} | 0 15 files changed, 236 insertions(+), 82 deletions(-) create mode 100644 configuration.example.toml create mode 100644 configuration.toml create mode 100644 src/main/kotlin/app/revanced/api/APISchema.kt create mode 100644 src/main/kotlin/app/revanced/api/ConfigurationSchema.kt rename src/main/resources/static/{about.json => api/about} (100%) diff --git a/.env.example b/.env.example index 6129bbb8..2bdac943 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ GITHUB_TOKEN= -API_VERSION= \ No newline at end of file +CONFIG_FILE_PATH= \ No newline at end of file diff --git a/.gitignore b/.gitignore index d16fa9ed..dd55c736 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ .vscode/ ### Project ### -.env \ No newline at end of file +.env +configuration.toml \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 822a45fa..d1c7c335 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,10 @@ dependencies { implementation(libs.exposed.core) implementation(libs.exposed.jdbc) implementation(libs.dotenv.kotlin) + implementation(libs.ktoml.core) + implementation(libs.ktoml.file) + testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) + } diff --git a/configuration.example.toml b/configuration.example.toml new file mode 100644 index 00000000..5935fd61 --- /dev/null +++ b/configuration.example.toml @@ -0,0 +1,5 @@ +organization = "org" +patches-repository = "patches" +integrations-repositories = ["integrations"] +contributors-repositories = ["patches", "integrations"] +api-version = 1 diff --git a/configuration.toml b/configuration.toml new file mode 100644 index 00000000..dcef633e --- /dev/null +++ b/configuration.toml @@ -0,0 +1,14 @@ +organization = "revanced" +patches-repository = "revanced-patches" +integrations-repositories = [ + "revanced-integrations" +] +contributors-repositories = [ + "revanced-patcher", + "revanced-patches", + "revanced-integrations", + "revanced-website", + "revanced-cli", + "revanced-manager", +] +api-version = 1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae96f6ab..5074e916 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ h2="2.1.214" koin="3.5.3" dotenv="6.4.1" ktor = "2.3.7" +ktoml = "0.5.1" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -34,6 +35,8 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" } ktor-server-tests = { module = "io.ktor:ktor-server-tests" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } +ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/APISchema.kt b/src/main/kotlin/app/revanced/api/APISchema.kt new file mode 100644 index 00000000..5e2de99e --- /dev/null +++ b/src/main/kotlin/app/revanced/api/APISchema.kt @@ -0,0 +1,55 @@ +package app.revanced.api + +import kotlinx.serialization.Serializable + +@Serializable +class APIRelease( + val version: String, + val createdAt: String, + val changelog: String, + val assets: Set +) + +interface APIUser { + val name: String + val avatarUrl: String + val url: String +} + +@Serializable +class APIMember( + override val name: String, + override val avatarUrl: String, + override val url: String, + val gpgKeysUrl: String +) : APIUser + +@Serializable +class APIContributor( + override val name: String, + override val avatarUrl: String, + override val url: String, + val contributions: Int, +) : APIUser + +@Serializable +class APIContributable( + val name: String, + val contributors: Set +) + +@Serializable +class APIAsset( + val downloadUrl: String, +) { + val type = when { + downloadUrl.endsWith(".jar") -> "patches" + downloadUrl.endsWith(".apk") -> "integrations" + else -> "unknown" + } +} + +@Serializable +class APIReleaseVersion( + val version: String +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/Application.kt b/src/main/kotlin/app/revanced/api/Application.kt index d7dd38b4..0b7c5712 100644 --- a/src/main/kotlin/app/revanced/api/Application.kt +++ b/src/main/kotlin/app/revanced/api/Application.kt @@ -7,8 +7,6 @@ import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { - Dotenv.load() - embeddedServer(Netty, port = 8080, host = "0.0.0.0", configure = { connectionGroupSize = 1 workerGroupSize = 1 diff --git a/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt b/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt new file mode 100644 index 00000000..0a5979a2 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt @@ -0,0 +1,17 @@ +package app.revanced.api + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class APIConfiguration( + val organization: String, + @SerialName("patches-repository") + val patchesRepository: String, + @SerialName("integrations-repositories") + val integrationsRepositoryNames: Set, + @SerialName("contributors-repositories") + val contributorsRepositoryNames: Set, + @SerialName("api-version") + val apiVersion: Int = 1 +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/backend/Backend.kt index efb1d02b..6549c64b 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/backend/Backend.kt @@ -19,12 +19,12 @@ abstract class Backend( * * @property name The name of the user. * @property avatarUrl The URL to the avatar of the user. - * @property profileUrl The URL to the profile of the user. + * @property url The URL to the profile of the user. */ - interface User { + interface BackendUser { val name: String val avatarUrl: String - val profileUrl: String + val url: String } /** @@ -32,48 +32,50 @@ abstract class Backend( * * @property members The members of the organization. */ - class Organization( - val members: Set + class BackendOrganization( + val members: Set ) { /** * A member of an organization. * * @property name The name of the member. * @property avatarUrl The URL to the avatar of the member. - * @property profileUrl The URL to the profile of the member. + * @property url The URL to the profile of the member. * @property bio The bio of the member. * @property gpgKeysUrl The URL to the GPG keys of the member. */ @Serializable - class Member ( + class BackendMember ( override val name: String, override val avatarUrl: String, - override val profileUrl: String, + override val url: String, val bio: String?, - val gpgKeysUrl: String? - ) : User + val gpgKeysUrl: String + ) : BackendUser /** * A repository of an organization. * * @property contributors The contributors of the repository. */ - class Repository( - val contributors: Set + class BackendRepository( + val contributors: Set ) { /** * A contributor of a repository. * * @property name The name of the contributor. * @property avatarUrl The URL to the avatar of the contributor. - * @property profileUrl The URL to the profile of the contributor. + * @property url The URL to the profile of the contributor. + * @property contributions The number of contributions of the contributor. */ @Serializable - class Contributor( + class BackendContributor( override val name: String, override val avatarUrl: String, - override val profileUrl: String - ) : User + override val url: String, + val contributions: Int + ) : BackendUser /** * A release of a repository. @@ -84,11 +86,11 @@ abstract class Backend( * @property releaseNote The release note of the release. */ @Serializable - class Release( + class BackendRelease( val tag: String, val releaseNote: String, val createdAt: String, - val assets: Set + val assets: Set ) { /** * An asset of a release. @@ -96,7 +98,7 @@ abstract class Backend( * @property downloadUrl The URL to download the asset. */ @Serializable - class Asset( + class BackendAsset( val downloadUrl: String ) } @@ -109,17 +111,13 @@ abstract class Backend( * @param owner The owner of the repository. * @param repository The name of the repository. * @param tag The tag of the release. If null, the latest release is returned. - * @param preRelease Whether to return a pre-release. - * If no pre-release exists, the latest release is returned. - * If tag is not null, this parameter is ignored. * @return The release. */ abstract suspend fun getRelease( owner: String, repository: String, tag: String? = null, - preRelease: Boolean = false - ): Organization.Repository.Release + ): BackendOrganization.BackendRepository.BackendRelease /** * Get the contributors of a repository. @@ -128,7 +126,7 @@ abstract class Backend( * @param repository The name of the repository. * @return The contributors. */ - abstract suspend fun getContributors(owner: String, repository: String): Set + abstract suspend fun getContributors(owner: String, repository: String): Set /** * Get the members of an organization. @@ -136,5 +134,5 @@ abstract class Backend( * @param organization The name of the organization. * @return The members. */ - abstract suspend fun getMembers(organization: String): Set + abstract suspend fun getMembers(organization: String): Set } diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt index db144f50..e024ce59 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -9,13 +9,17 @@ import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.resources.* +import app.revanced.api.backend.Backend.BackendOrganization.BackendMember +import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors import app.revanced.api.backend.github.api.Request.Organization.Members import app.revanced.api.backend.github.api.Response -import app.revanced.api.backend.github.api.Response.Organization.Repository.Release -import app.revanced.api.backend.github.api.Response.Organization.Repository.Contributor -import app.revanced.api.backend.github.api.Response.Organization.Member +import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease +import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember import io.ktor.client.plugins.resources.Resources import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* @@ -55,59 +59,58 @@ class GitHubBackend(token: String? = null) : Backend({ owner: String, repository: String, tag: String?, - preRelease: Boolean - ): Organization.Repository.Release { - val release = if (preRelease) { - val releases: Set = client.get(Releases(owner, repository)).body() - releases.firstOrNull { it.preReleases } ?: releases.first() // Latest pre-release or latest release - } else { - client.get( - tag?.let { Releases.Tag(owner, repository, it) } - ?: Releases.Latest(owner, repository) - ).body() - } + ): BackendRelease { + val release: GitHubRelease = if (tag != null) + client.get(Releases.Tag(owner, repository, tag)).body() + else + client.get(Releases.Latest(owner, repository)).body() + - return Organization.Repository.Release( + return BackendRelease( tag = release.tagName, releaseNote = release.body, createdAt = release.createdAt, assets = release.assets.map { - Organization.Repository.Release.Asset( + BackendAsset( downloadUrl = it.browserDownloadUrl ) }.toSet() ) } - override suspend fun getContributors(owner: String, repository: String): Set { - val contributors: Set = client.get(Contributors(owner, repository)).body() + override suspend fun getContributors( + owner: String, + repository: String + ): Set { + val contributors: Set = client.get(Contributors(owner, repository)).body() return contributors.map { - Organization.Repository.Contributor( + BackendContributor( name = it.login, avatarUrl = it.avatarUrl, - profileUrl = it.url + url = it.url, + contributions = it.contributions ) }.toSet() } - override suspend fun getMembers(organization: String): Set { + override suspend fun getMembers(organization: String): Set { // Get the list of members of the organization. - val members: Set = client.get(Members(organization)).body>() + val members: Set = client.get(Members(organization)).body() return runBlocking(Dispatchers.Default) { members.map { member -> // Map the member to a user in order to get the bio. async { - client.get(Request.User(member.login)).body() + client.get(Request.User(member.login)).body() } } }.awaitAll().map { user -> // Map the user back to a member. - Organization.Member( + BackendMember( name = user.login, avatarUrl = user.avatarUrl, - profileUrl = user.url, + url = user.url, bio = user.bio, gpgKeysUrl = "https://github.com/${user.login}.gpg", ) diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt index e286b957..26d56b5a 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt @@ -1,49 +1,50 @@ package app.revanced.api.backend.github.api +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable class Response { - interface IUser { + interface IGitHubUser { val login: String val avatarUrl: String val url: String } @Serializable - class User ( + class GitHubUser ( override val login: String, override val avatarUrl: String, override val url: String, val bio: String?, - ) : IUser + ) : IGitHubUser - class Organization { + class GitHubOrganization { @Serializable - class Member( + class GitHubMember( override val login: String, override val avatarUrl: String, override val url: String, - ) : IUser + ) : IGitHubUser - class Repository { + class GitHubRepository { @Serializable - class Contributor( + class GitHubContributor( override val login: String, override val avatarUrl: String, override val url: String, - ) : IUser + val contributions: Int, + ) : IGitHubUser @Serializable - class Release( + class GitHubRelease( val tagName: String, - val assets: Set, - val preReleases: Boolean, + val assets: Set, val createdAt: String, val body: String ) { @Serializable - class Asset( + class GitHubAsset( val browserDownloadUrl: String ) } diff --git a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt index 3950ec9d..7e0a3d3b 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt @@ -1,21 +1,37 @@ package app.revanced.api.plugins +import app.revanced.api.APIConfiguration import app.revanced.api.backend.github.GitHubBackend +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.source.decodeFromStream import io.github.cdimascio.dotenv.Dotenv import io.ktor.server.application.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import org.koin.core.context.startKoin import org.koin.dsl.module import org.koin.ktor.ext.inject import org.koin.ktor.plugin.Koin +import java.io.File fun Application.configureDependencies() { install(Koin) { modules( module { - single { Dotenv.load() } - single { GitHubBackend(get().get("GITHUB_TOKEN")) } + single { + Dotenv.load() + } + single { + val configFilePath = get().get("CONFIG_FILE_PATH")!! + Toml.decodeFromStream(File(configFilePath).inputStream()) + } + single { + val token = get().get("GITHUB_TOKEN") + GitHubBackend(token) + } } ) } } + diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt index c14a7c38..1bd019f6 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Routing.kt @@ -1,36 +1,74 @@ package app.revanced.api.plugins +import app.revanced.api.* import app.revanced.api.backend.github.GitHubBackend -import io.github.cdimascio.dotenv.Dotenv +import io.ktor.client.utils.EmptyContent.contentType import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.http.content.* import io.ktor.server.response.* import io.ktor.server.routing.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import org.koin.ktor.ext.inject fun Application.configureRouting() { val backend by inject() - val dotenv by inject() + val configuration by inject() routing { - route("/v${dotenv.get("API_VERSION", "1")}") { - route("/manager") { - get("/contributors") { - val contributors = backend.getContributors("revanced", "revanced-patches") + route("/v${configuration.apiVersion}") { + route("/patches") { + get { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + val integrations = configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() + + val assets = (patches.assets + integrations.flatMap { it.assets }).filter { + it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") + }.map { APIAsset(it.downloadUrl) }.toSet() + + val release = APIRelease( + patches.tag, + patches.createdAt, + patches.releaseNote, + assets + ) - call.respond(contributors) + call.respond(release) } - get("/members") { - val members = backend.getMembers("revanced") + get("/version") { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - call.respond(members) + val release = APIReleaseVersion(patches.tag) + + call.respond(release) } } - route("/patches") { + get("/contributors") { + val contributors = configuration.contributorsRepositoryNames.map { + async { + APIContributable( + it, + backend.getContributors(configuration.organization, it).map { + APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + }.toSet() + ) + } + }.awaitAll() + call.respond(contributors) + } + + get("/members") { + val members = backend.getMembers(configuration.organization).map { + APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + } + + call.respond(members) } route("/ping") { @@ -38,8 +76,9 @@ fun Application.configureRouting() { call.respond(HttpStatusCode.NoContent) } } + + staticResources("/", "/static/api") { contentType { ContentType.Application.Json } } } - staticResources("/", "static") } } diff --git a/src/main/resources/static/about.json b/src/main/resources/static/api/about similarity index 100% rename from src/main/resources/static/about.json rename to src/main/resources/static/api/about From a988ffbd2303a79ee18be7263ef6cd45c7bc4daf Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 03:46:17 +0100 Subject: [PATCH 03/81] feat: Add CLI --- build.gradle.kts | 9 ++++- gradle/libs.versions.toml | 2 + .../kotlin/app/revanced/api/Application.kt | 22 ----------- .../app/revanced/api/command/MainCommand.kt | 34 ++++++++++++++++ .../revanced/api/command/StartAPICommand.kt | 39 +++++++++++++++++++ .../app/revanced/api/plugins/Dependencies.kt | 6 +-- .../app/revanced/api/plugins/Routing.kt | 3 +- .../revanced/api/{ => schema}/APISchema.kt | 22 ++++++++++- .../api/{ => schema}/ConfigurationSchema.kt | 2 +- .../app.revanced.api/version.properties | 1 + 10 files changed, 107 insertions(+), 33 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/api/Application.kt create mode 100644 src/main/kotlin/app/revanced/api/command/MainCommand.kt create mode 100644 src/main/kotlin/app/revanced/api/command/StartAPICommand.kt rename src/main/kotlin/app/revanced/api/{ => schema}/APISchema.kt (72%) rename src/main/kotlin/app/revanced/api/{ => schema}/ConfigurationSchema.kt (93%) create mode 100644 src/main/resources/app.revanced.api/version.properties diff --git a/build.gradle.kts b/build.gradle.kts index d1c7c335..bfc4d3f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,12 +7,18 @@ plugins { group = "app.revanced" application { - mainClass.set("app.revanced.api.ApplicationKt") + mainClass.set("app.revanced.api.command.MainCommandKt") val isDevelopment: Boolean = project.ext.has("development") applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") } +tasks { + processResources { + expand("projectVersion" to project.version) + } +} + repositories { mavenCentral() } @@ -44,6 +50,7 @@ dependencies { implementation(libs.dotenv.kotlin) implementation(libs.ktoml.core) implementation(libs.ktoml.file) + implementation(libs.picocli) testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5074e916..413b850f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ koin="3.5.3" dotenv="6.4.1" ktor = "2.3.7" ktoml = "0.5.1" +picocli = "4.7.3" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -37,6 +38,7 @@ ktor-server-tests = { module = "io.ktor:ktor-server-tests" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } +picocli = { module = "info.picocli:picocli", version.ref = "picocli" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/Application.kt b/src/main/kotlin/app/revanced/api/Application.kt deleted file mode 100644 index 0b7c5712..00000000 --- a/src/main/kotlin/app/revanced/api/Application.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.revanced.api - -import app.revanced.api.plugins.* -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* - -fun main() { - embeddedServer(Netty, port = 8080, host = "0.0.0.0", configure = { - connectionGroupSize = 1 - workerGroupSize = 1 - callGroupSize = 1 - }) { - configureHTTP() - configureSerialization() - configureDatabases() - configureSecurity() - configureDependencies() - configureRouting() - }.start(wait = true) -} diff --git a/src/main/kotlin/app/revanced/api/command/MainCommand.kt b/src/main/kotlin/app/revanced/api/command/MainCommand.kt new file mode 100644 index 00000000..3038e963 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/command/MainCommand.kt @@ -0,0 +1,34 @@ +package app.revanced.api.command + +import picocli.CommandLine +import java.util.* + +fun main(args: Array) { + CommandLine(MainCommand).execute(*args).let(System::exit) +} + +private object CLIVersionProvider : CommandLine.IVersionProvider { + override fun getVersion() = + arrayOf( + MainCommand::class.java.getResourceAsStream( + "/app/revanced/api/version.properties", + )?.use { stream -> + Properties().apply { + load(stream) + }.let { + "ReVanced API v${it.getProperty("version")}" + } + } ?: "ReVanced API", + ) +} + +@CommandLine.Command( + name = "revanced-api", + description = ["API server for ReVanced"], + mixinStandardHelpOptions = true, + versionProvider = CLIVersionProvider::class, + subcommands = [ + StartAPICommand::class, + ], +) +private object MainCommand \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt new file mode 100644 index 00000000..be7abfb8 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -0,0 +1,39 @@ +package app.revanced.api.command + +import app.revanced.api.plugins.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import picocli.CommandLine + +@CommandLine.Command( + name = "start", + description = ["Start the API server"], +) +internal object StartAPICommand : Runnable { + @CommandLine.Option( + names = ["-h", "--host"], + description = ["The host address to bind to."], + ) + private var host: String = "0.0.0.0" + + @CommandLine.Option( + names = ["-p", "--port"], + description = ["The port to listen on."], + ) + private var port: Int = 8080 + + override fun run() { + embeddedServer(Netty, port, host, configure = { + connectionGroupSize = 1 + workerGroupSize = 1 + callGroupSize = 1 + }) { + configureHTTP() + configureSerialization() + configureDatabases() + configureSecurity() + configureDependencies() + configureRouting() + }.start(wait = true) + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt index 7e0a3d3b..7667e3fb 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt @@ -1,16 +1,12 @@ package app.revanced.api.plugins -import app.revanced.api.APIConfiguration +import app.revanced.api.schema.APIConfiguration import app.revanced.api.backend.github.GitHubBackend import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.source.decodeFromStream import io.github.cdimascio.dotenv.Dotenv import io.ktor.server.application.* -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import org.koin.core.context.startKoin import org.koin.dsl.module -import org.koin.ktor.ext.inject import org.koin.ktor.plugin.Koin import java.io.File diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt index 1bd019f6..8736aff8 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Routing.kt @@ -1,8 +1,7 @@ package app.revanced.api.plugins -import app.revanced.api.* import app.revanced.api.backend.github.GitHubBackend -import io.ktor.client.utils.EmptyContent.contentType +import app.revanced.api.schema.* import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.http.content.* diff --git a/src/main/kotlin/app/revanced/api/APISchema.kt b/src/main/kotlin/app/revanced/api/schema/APISchema.kt similarity index 72% rename from src/main/kotlin/app/revanced/api/APISchema.kt rename to src/main/kotlin/app/revanced/api/schema/APISchema.kt index 5e2de99e..44324705 100644 --- a/src/main/kotlin/app/revanced/api/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/APISchema.kt @@ -1,4 +1,4 @@ -package app.revanced.api +package app.revanced.api.schema import kotlinx.serialization.Serializable @@ -52,4 +52,22 @@ class APIAsset( @Serializable class APIReleaseVersion( val version: String -) \ No newline at end of file +) + +@Serializable +class APIAnnouncement( + val id: Int, + val author: APIUser?, + val title: String, + val content: APIAnnouncementContent, + val channel: String, + val createdAt: String, + val archivedAt: String?, + val level: Int, +) + +@Serializable +class APIAnnouncementContent( + val message: String, + val attachmentUrls: Set +) diff --git a/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt b/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt similarity index 93% rename from src/main/kotlin/app/revanced/api/ConfigurationSchema.kt rename to src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt index 0a5979a2..db90377a 100644 --- a/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt @@ -1,4 +1,4 @@ -package app.revanced.api +package app.revanced.api.schema import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/src/main/resources/app.revanced.api/version.properties b/src/main/resources/app.revanced.api/version.properties new file mode 100644 index 00000000..308c9f8e --- /dev/null +++ b/src/main/resources/app.revanced.api/version.properties @@ -0,0 +1 @@ +version=${projectVersion} \ No newline at end of file From 4dffd32c99a5a3deafe21bc9e9960795ff93ff1d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 03:47:36 +0100 Subject: [PATCH 04/81] fix: Use correct resource path --- .../{app.revanced.api => app/revanced/api}/version.properties | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/{app.revanced.api => app/revanced/api}/version.properties (100%) diff --git a/src/main/resources/app.revanced.api/version.properties b/src/main/resources/app/revanced/api/version.properties similarity index 100% rename from src/main/resources/app.revanced.api/version.properties rename to src/main/resources/app/revanced/api/version.properties From db0bfc3be5b8d86466fa7f1a01a72247b4e878aa Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 03:47:56 +0100 Subject: [PATCH 05/81] feat: Show default CLI option values --- src/main/kotlin/app/revanced/api/command/StartAPICommand.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index be7abfb8..0aa50eda 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -13,12 +13,14 @@ internal object StartAPICommand : Runnable { @CommandLine.Option( names = ["-h", "--host"], description = ["The host address to bind to."], + showDefaultValue = CommandLine.Help.Visibility.ALWAYS, ) private var host: String = "0.0.0.0" @CommandLine.Option( names = ["-p", "--port"], description = ["The port to listen on."], + showDefaultValue = CommandLine.Help.Visibility.ALWAYS, ) private var port: Int = 8080 From 80c8083610fbcc30341ae392463d6141a3623bf3 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 10:57:47 +0100 Subject: [PATCH 06/81] chore: Specify extension for static API files --- .../app/revanced/api/plugins/Routing.kt | 5 +- .../static/api/{about => about.json} | 48 +++++++------------ 2 files changed, 20 insertions(+), 33 deletions(-) rename src/main/resources/static/api/{about => about.json} (65%) diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt index 8736aff8..39764006 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Routing.kt @@ -76,7 +76,10 @@ fun Application.configureRouting() { } } - staticResources("/", "/static/api") { contentType { ContentType.Application.Json } } + staticResources("/", "/static/api") { + contentType { ContentType.Application.Json } + extensions("json") + } } } diff --git a/src/main/resources/static/api/about b/src/main/resources/static/api/about.json similarity index 65% rename from src/main/resources/static/api/about rename to src/main/resources/static/api/about.json index 8f62d263..1a0d1cbf 100644 --- a/src/main/resources/static/api/about +++ b/src/main/resources/static/api/about.json @@ -1,16 +1,13 @@ { "name": "ReVanced", "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", - "branding": - { + "branding": { "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" }, - "contact": - { + "contact": { "email": "contact@revanced.app" }, - "socials": - [ + "socials": [ { "name": "Website", "url": "https://revanced.app", @@ -18,13 +15,11 @@ }, { "name": "GitHub", - "url": "https://github.com/revanced", - "preferred": false + "url": "https://github.com/revanced" }, { "name": "Twitter", - "url": "https://twitter.com/revancedapp", - "preferred": false + "url": "https://twitter.com/revancedapp" }, { "name": "Discord", @@ -33,29 +28,23 @@ }, { "name": "Reddit", - "url": "https://www.reddit.com/r/revancedapp", - "preferred": false + "url": "https://www.reddit.com/r/revancedapp" }, { "name": "Telegram", - "url": "https://t.me/app_revanced", - "preferred": false + "url": "https://t.me/app_revanced" }, { "name": "YouTube", - "url": "https://www.youtube.com/@ReVanced", - "preferred": false + "url": "https://www.youtube.com/@ReVanced" } ], - "donations": - { - "wallets": - [ + "donations": { + "wallets": [ { "network": "Bitcoin", "currency_code": "BTC", - "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f", - "preferred": false + "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f" }, { "network": "Dogecoin", @@ -66,24 +55,20 @@ { "network": "Ethereum", "currency_code": "ETH", - "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5", - "preferred": false + "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5" }, { "network": "Litecoin", "currency_code": "LTC", - "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2", - "preferred": false + "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2" }, { "network": "Monero", "currency_code": "XMR", - "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh", - "preferred": false + "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh" } ], - "links": - [ + "links": [ { "name": "Open Collective", "url": "https://opencollective.com/revanced", @@ -91,8 +76,7 @@ }, { "name": "GitHub Sponsors", - "url": "https://github.com/sponsors/ReVanced", - "preferred": false + "url": "https://github.com/sponsors/ReVanced" } ] } From df999c00c4c1f8645cc67ae19b732f0af9d71823 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 10:58:15 +0100 Subject: [PATCH 07/81] feat: Improve routing paths --- .../app/revanced/api/plugins/Routing.kt | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt index 39764006..01382077 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Routing.kt @@ -18,32 +18,34 @@ fun Application.configureRouting() { routing { route("/v${configuration.apiVersion}") { route("/patches") { - get { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrations = configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = (patches.assets + integrations.flatMap { it.assets }).filter { - it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") - }.map { APIAsset(it.downloadUrl) }.toSet() - - val release = APIRelease( - patches.tag, - patches.createdAt, - patches.releaseNote, - assets - ) - - call.respond(release) - } + route("latest") { + get { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + val integrations = configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() + + val assets = (patches.assets + integrations.flatMap { it.assets }).filter { + it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") + }.map { APIAsset(it.downloadUrl) }.toSet() + + val release = APIRelease( + patches.tag, + patches.createdAt, + patches.releaseNote, + assets + ) - get("/version") { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + call.respond(release) + } - val release = APIReleaseVersion(patches.tag) + get("/version") { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - call.respond(release) + val release = APIReleaseVersion(patches.tag) + + call.respond(release) + } } } From af0b0865f4c2f22975a836b72ff0b902ddac1ce9 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 29 Jan 2024 11:31:15 +0100 Subject: [PATCH 08/81] feat: Remove Swagger and OpenAPI Support and features are lacking --- build.gradle.kts | 3 - docs/.swagger-codegen-ignore | 23 - docs/.swagger-codegen/VERSION | 1 - docs/index.html | 2752 ----------------- gradle/libs.versions.toml | 2 - .../kotlin/app/revanced/api/plugins/HTTP.kt | 17 +- src/main/resources/openapi/documentation.yaml | 23 - 7 files changed, 3 insertions(+), 2818 deletions(-) delete mode 100644 docs/.swagger-codegen-ignore delete mode 100644 docs/.swagger-codegen/VERSION delete mode 100644 docs/index.html delete mode 100644 src/main/resources/openapi/documentation.yaml diff --git a/build.gradle.kts b/build.gradle.kts index bfc4d3f4..50cccda8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,8 +34,6 @@ dependencies { implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.server.auth) implementation(libs.ktor.server.auth.jwt) - implementation(libs.ktor.server.swagger) - implementation(libs.ktor.server.openapi) implementation(libs.ktor.server.cors) implementation(libs.ktor.server.caching.headers) implementation(libs.ktor.server.host.common) @@ -54,5 +52,4 @@ dependencies { testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) - } diff --git a/docs/.swagger-codegen-ignore b/docs/.swagger-codegen-ignore deleted file mode 100644 index c5fa491b..00000000 --- a/docs/.swagger-codegen-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# Swagger Codegen Ignore -# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/docs/.swagger-codegen/VERSION b/docs/.swagger-codegen/VERSION deleted file mode 100644 index a254f0ac..00000000 --- a/docs/.swagger-codegen/VERSION +++ /dev/null @@ -1 +0,0 @@ -3.0.41 \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index af14f149..00000000 --- a/docs/index.html +++ /dev/null @@ -1,2752 +0,0 @@ - - - - - Application API - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 413b850f..016787bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,8 +21,6 @@ ktor-server-core = { module = "io.ktor:ktor-server-core" } ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" } ktor-server-auth = { module = "io.ktor:ktor-server-auth" } ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" } -ktor-server-swagger = { module = "io.ktor:ktor-server-swagger" } -ktor-server-openapi = { module = "io.ktor:ktor-server-openapi" } ktor-server-cors = { module = "io.ktor:ktor-server-cors" } ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } diff --git a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt b/src/main/kotlin/app/revanced/api/plugins/HTTP.kt index 8ea4b73f..b46f1686 100644 --- a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/plugins/HTTP.kt @@ -6,18 +6,10 @@ import io.ktor.server.application.* import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.conditionalheaders.* import io.ktor.server.plugins.cors.routing.* -import io.ktor.server.plugins.openapi.* -import io.ktor.server.plugins.swagger.* -import io.ktor.server.routing.* +import kotlin.time.Duration.Companion.minutes fun Application.configureHTTP() { install(ConditionalHeaders) - routing { - swaggerUI(path = "openapi") - } - routing { - openAPI(path = "openapi") - } install(CORS) { allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Put) @@ -27,11 +19,8 @@ fun Application.configureHTTP() { anyHost() // @TODO: Don't do this in production if possible. Try to limit it. } install(CachingHeaders) { - options { _, outgoingContent -> - when (outgoingContent.contentType?.withoutParameters()) { - ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60)) - else -> null - } + options { _, _ -> + CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } } } diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml deleted file mode 100644 index 2e6c6d76..00000000 --- a/src/main/resources/openapi/documentation.yaml +++ /dev/null @@ -1,23 +0,0 @@ -openapi: "3.0.3" -info: - title: "Application API" - description: "Application API" - version: "1.0.0" -servers: - - url: "http://0.0.0.0:8080" -paths: - /: - get: - description: "Hello World!" - responses: - "200": - description: "OK" - content: - text/plain: - schema: - type: "string" - examples: - Example#1: - value: "Hello World!" -components: - schemas: From 42f731854d0b91070bd7b075054d67d3d9e1d1b4 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 31 Jan 2024 03:01:07 +0100 Subject: [PATCH 09/81] feat: Add announcements API --- .env.example | 13 +- build.gradle.kts | 3 + configuration.example.toml | 2 +- gradle/libs.versions.toml | 16 +- .../app/revanced/api/backend/Backend.kt | 3 +- .../api/backend/github/GitHubBackend.kt | 2 + .../api/backend/github/api/ResponseSchema.kt | 3 +- .../revanced/api/command/StartAPICommand.kt | 5 +- .../app/revanced/api/modules/Database.kt | 160 ++++++++++++++++++ .../app/revanced/api/modules/Dependencies.kt | 73 ++++++++ .../revanced/api/{plugins => modules}/HTTP.kt | 2 +- .../app/revanced/api/modules/Routing.kt | 158 +++++++++++++++++ .../app/revanced/api/modules/Security.kt | 53 ++++++ .../api/{plugins => modules}/Serialization.kt | 2 +- .../app/revanced/api/plugins/Databases.kt | 49 ------ .../app/revanced/api/plugins/Dependencies.kt | 33 ---- .../app/revanced/api/plugins/Routing.kt | 88 ---------- .../app/revanced/api/plugins/Security.kt | 30 ---- .../app/revanced/api/plugins/UsersSchema.kt | 59 ------- .../app/revanced/api/schema/APISchema.kt | 37 ++-- .../kotlin/app/revanced/ApplicationTest.kt | 2 +- 21 files changed, 508 insertions(+), 285 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/modules/Database.kt create mode 100644 src/main/kotlin/app/revanced/api/modules/Dependencies.kt rename src/main/kotlin/app/revanced/api/{plugins => modules}/HTTP.kt (96%) create mode 100644 src/main/kotlin/app/revanced/api/modules/Routing.kt create mode 100644 src/main/kotlin/app/revanced/api/modules/Security.kt rename src/main/kotlin/app/revanced/api/{plugins => modules}/Serialization.kt (87%) delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Databases.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Dependencies.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Routing.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Security.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt diff --git a/.env.example b/.env.example index 2bdac943..91a98197 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,13 @@ GITHUB_TOKEN= -CONFIG_FILE_PATH= \ No newline at end of file +CONFIG_FILE_PATH=configuration.toml + +DB_URL=jdbc:h2:./api.db +DB_USER= +DB_PASSWORD= + +JWT_SECRET= +JWT_ISSUER= +JWT_VALIDITY_IN_MIN= + +BASIC_USERNAME= +BASIC_PASSWORD= diff --git a/build.gradle.kts b/build.gradle.kts index 50cccda8..b932b6f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,10 +45,13 @@ dependencies { implementation(libs.logback.classic) implementation(libs.exposed.core) implementation(libs.exposed.jdbc) + implementation(libs.exposed.dao) + implementation(libs.exposed.kotlin.datetime) implementation(libs.dotenv.kotlin) implementation(libs.ktoml.core) implementation(libs.ktoml.file) implementation(libs.picocli) + implementation(libs.kotlinx.datetime) testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) diff --git a/configuration.example.toml b/configuration.example.toml index 5935fd61..0fdadb02 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -2,4 +2,4 @@ organization = "org" patches-repository = "patches" integrations-repositories = ["integrations"] contributors-repositories = ["patches", "integrations"] -api-version = 1 +api-version = 1 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 016787bc..f5d36559 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,14 @@ [versions] -kotlin="1.9.22" -logback="1.4.14" -exposed="0.41.1" -h2="2.1.214" -koin="3.5.3" -dotenv="6.4.1" +kotlin = "1.9.22" +logback = "1.4.14" +exposed = "0.41.1" +h2 = "2.2.224" +koin = "3.5.3" +dotenv = "6.4.1" ktor = "2.3.7" ktoml = "0.5.1" picocli = "4.7.3" +datetime = "0.5.0" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -31,12 +32,15 @@ h2 = { module = "com.h2database:h2", version.ref = "h2" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } +exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" } dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" } ktor-server-tests = { module = "io.ktor:ktor-server-tests" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/backend/Backend.kt index 6549c64b..e2319493 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/backend/Backend.kt @@ -2,6 +2,7 @@ package app.revanced.api.backend import io.ktor.client.* import io.ktor.client.engine.okhttp.* +import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable /** @@ -89,7 +90,7 @@ abstract class Backend( class BackendRelease( val tag: String, val releaseNote: String, - val createdAt: String, + val createdAt: LocalDateTime, val assets: Set ) { /** diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt index e024ce59..10dcfdaa 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy +import org.koin.dsl.bind +import org.koin.dsl.module @OptIn(ExperimentalSerializationApi::class) class GitHubBackend(token: String? = null) : Backend({ diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt index 26d56b5a..9fe86ab5 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt @@ -1,5 +1,6 @@ package app.revanced.api.backend.github.api +import kotlinx.datetime.LocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -40,7 +41,7 @@ class Response { class GitHubRelease( val tagName: String, val assets: Set, - val createdAt: String, + val createdAt: LocalDateTime, val body: String ) { @Serializable diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 0aa50eda..b4e3cd6e 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -1,6 +1,6 @@ package app.revanced.api.command -import app.revanced.api.plugins.* +import app.revanced.api.modules.* import io.ktor.server.engine.* import io.ktor.server.netty.* import picocli.CommandLine @@ -30,11 +30,10 @@ internal object StartAPICommand : Runnable { workerGroupSize = 1 callGroupSize = 1 }) { + configureDependencies() configureHTTP() configureSerialization() - configureDatabases() configureSecurity() - configureDependencies() configureRouting() }.start(wait = true) } diff --git a/src/main/kotlin/app/revanced/api/modules/Database.kt b/src/main/kotlin/app/revanced/api/modules/Database.kt new file mode 100644 index 00000000..fcd09551 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Database.kt @@ -0,0 +1,160 @@ +package app.revanced.api.modules + +import app.revanced.api.modules.AnnouncementService.Attachments.announcement +import app.revanced.api.schema.APIResponseAnnouncement +import app.revanced.api.schema.APIAnnouncement +import app.revanced.api.schema.APILatestAnnouncement +import kotlinx.datetime.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.kotlin.datetime.datetime + + +class AnnouncementService(private val database: Database) { + private object Announcements : IntIdTable() { + val author = varchar("author", 32).nullable() + val title = varchar("title", 64) + val content = text("content").nullable() + val channel = varchar("channel", 16).nullable() + val createdAt = datetime("createdAt") + val archivedAt = datetime("archivedAt").nullable() + val level = integer("level") + } + + private object Attachments : IntIdTable() { + val url = varchar("url", 256) + val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) + } + + class Announcement(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Announcements) + + var author by Announcements.author + var title by Announcements.title + var content by Announcements.content + val attachments by Attachment referrersOn announcement + var channel by Announcements.channel + var createdAt by Announcements.createdAt + var archivedAt by Announcements.archivedAt + var level by Announcements.level + + fun api() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachments.map(Attachment::url).toSet(), + channel, + createdAt, + archivedAt, + level + ) + } + + class Attachment(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Attachments) + + var url by Attachments.url + var announcement by Announcement referencedOn Attachments.announcement + } + + init { + transaction { + SchemaUtils.create(Announcements, Attachments) + } + } + + private fun transaction(block: Transaction.() -> T) = transaction(database, block) + + fun read() = transaction { + Announcement.all().map { it.api() }.toSet() + } + + fun read(channel: String) = transaction { + Announcement.find { Announcements.channel eq channel }.map { it.api() }.toSet() + } + + fun delete(id: Int) = transaction { + val announcement = Announcement.findById(id) ?: return@transaction + + announcement.delete() + } + + fun latest() = transaction { + Announcement.all().maxByOrNull { it.createdAt }?.api() + } + + fun latest(channel: String) = transaction { + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.api() + } + + fun latestId() = transaction { + Announcement.all().maxByOrNull { it.createdAt }?.id?.value?.let { + APILatestAnnouncement(it) + } + } + + fun latestId(channel: String) = transaction { + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { + APILatestAnnouncement(it) + } + } + + fun archive( + id: Int, + archivedAt: LocalDateTime? + ) = transaction { + Announcement.findById(id)?.apply { + this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() + } + } + + fun unarchive(id: Int) = transaction { + Announcement.findById(id)?.apply { + archivedAt = null + } + } + + fun new(new: APIAnnouncement) = transaction { + Announcement.new announcement@{ + author = new.author + title = new.title + content = new.content + channel = new.channel + createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + archivedAt = new.archivedAt + level = new.level + }.also { newAnnouncement -> + new.attachmentUrls.map { + Attachment.new { + url = it + announcement = newAnnouncement + } + } + } + } + + fun update(id: Int, new: APIAnnouncement) = transaction { + Announcement.findById(id)?.apply { + author = new.author + title = new.title + content = new.content + channel = new.channel + archivedAt = new.archivedAt + level = new.level + + attachments.forEach(Attachment::delete) + new.attachmentUrls.map { + Attachment.new { + url = it + announcement = this@apply + } + } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt new file mode 100644 index 00000000..48ee6c78 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt @@ -0,0 +1,73 @@ +package app.revanced.api.modules + +import app.revanced.api.backend.Backend +import app.revanced.api.backend.github.GitHubBackend +import app.revanced.api.schema.APIConfiguration +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.source.decodeFromStream +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.server.application.* +import org.jetbrains.exposed.sql.Database +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.ktor.plugin.Koin +import java.io.File + +fun Application.configureDependencies() { + install(Koin) { + modules( + globalModule, + gitHubBackendModule, + databaseModule, + authModule + ) + } +} + +val globalModule = module { + single { + Dotenv.load() + } + single { + val configFilePath = get().get("CONFIG_FILE_PATH")!! + Toml.decodeFromStream(File(configFilePath).inputStream()) + } +} + +val gitHubBackendModule = module { + single { + val token = get().get("GITHUB_TOKEN") + GitHubBackend(token) + } bind Backend::class +} + +val databaseModule = module { + single { + val dotenv = get() + + Database.connect( + url = dotenv.get("DB_URL"), + user = dotenv.get("DB_USER"), + password = dotenv.get("DB_PASSWORD"), + driver = "org.h2.Driver" + ) + } + factory { + AnnouncementService(get()) + } +} + +val authModule = module { + single { + val dotenv = get() + + val jwtSecret = dotenv.get("JWT_SECRET")!! + val issuer = dotenv.get("JWT_ISSUER")!! + val validityInMin = dotenv.get("JWT_VALIDITY_IN_MIN")!!.toInt() + + val basicUsername = dotenv.get("BASIC_USERNAME")!! + val basicPassword = dotenv.get("BASIC_PASSWORD")!! + + AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt b/src/main/kotlin/app/revanced/api/modules/HTTP.kt similarity index 96% rename from src/main/kotlin/app/revanced/api/plugins/HTTP.kt rename to src/main/kotlin/app/revanced/api/modules/HTTP.kt index b46f1686..590629ba 100644 --- a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/modules/HTTP.kt @@ -1,4 +1,4 @@ -package app.revanced.api.plugins +package app.revanced.api.modules import io.ktor.http.* import io.ktor.http.content.* diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt new file mode 100644 index 00000000..18da3811 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -0,0 +1,158 @@ +package app.revanced.api.modules + +import app.revanced.api.backend.Backend +import app.revanced.api.schema.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.util.pipeline.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.datetime.toKotlinLocalDateTime +import java.time.LocalDateTime +import org.koin.ktor.ext.get as koinGet + +fun Application.configureRouting() { + val backend: Backend = koinGet() + val configuration: APIConfiguration = koinGet() + val announcementService: AnnouncementService = koinGet() + val authService: AuthService = koinGet() + + routing { + route("/v${configuration.apiVersion}") { + route("/announcements") { + suspend fun PipelineContext<*, ApplicationCall>.announcement( + block: AnnouncementService.() -> APIResponseAnnouncement? + ) = announcementService.block()?.let { call.respond(it) } + ?: call.respond(HttpStatusCode.NotFound) + + suspend fun PipelineContext<*, ApplicationCall>.announcementId( + block: AnnouncementService.() -> APILatestAnnouncement? + ) = announcementService.block()?.let { call.respond(it) } + ?: call.respond(HttpStatusCode.NotFound) + + suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) = + block(call.parameters["channel"]!!) + + announcementService.new( + APIAnnouncement( + "author", + "title", + "content", + setOf("https://example.com"), + "channel", + LocalDateTime.now().toKotlinLocalDateTime(), + ) + ) + route("/{channel}/latest") { + get("/id") { channel { announcementId { latestId(it) } } } + + get { channel { announcement { latest(it) } } } + } + + get("/{channel}") { channel { call.respond(announcementService.read(it)) } } + + route("/latest") { + get("/id") { announcementId { latestId() } } + + get { announcement { latest() } } + } + + get { call.respond(announcementService.read()) } + + authenticate("jwt") { + suspend fun PipelineContext<*, ApplicationCall>.id(block: suspend (Int) -> Unit) = + call.parameters["id"]!!.toIntOrNull()?.let { block(it) } + ?: call.respond(HttpStatusCode.BadRequest) + + post { announcementService.new(call.receive()) } + + post("/{id}/archive") { + id { + val archivedAt = call.receiveNullable()?.archivedAt + announcementService.archive(it, archivedAt) + } + } + + post("/{id}/unarchive") { id { announcementService.unarchive(it) } } + + patch("/{id}") { id { announcementService.update(it, call.receive()) } } + + delete("/{id}") { id { announcementService.delete(it) } } + } + } + + route("/patches") { + route("latest") { + get { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + val integrations = configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() + + val assets = (patches.assets + integrations.flatMap { it.assets }).filter { + it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") + }.map { APIAsset(it.downloadUrl) }.toSet() + + val release = APIRelease( + patches.tag, + patches.createdAt, + patches.releaseNote, + assets + ) + + call.respond(release) + } + + get("/version") { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + + val release = APIReleaseVersion(patches.tag) + + call.respond(release) + } + } + } + + staticResources("/", "/static/api") { + contentType { ContentType.Application.Json } + extensions("json") + } + + get("/contributors") { + val contributors = configuration.contributorsRepositoryNames.map { + async { + APIContributable( + it, + backend.getContributors(configuration.organization, it).map { + APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + }.toSet() + ) + } + }.awaitAll() + + call.respond(contributors) + } + + get("/team") { + val team = backend.getMembers(configuration.organization).map { + APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + } + + call.respond(team) + } + + route("/ping") { + handle { + call.respond(HttpStatusCode.NoContent) + } + } + + authenticate("basic") { get("/token") { call.respond(authService.newToken()) } } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Security.kt b/src/main/kotlin/app/revanced/api/modules/Security.kt new file mode 100644 index 00000000..0dd2dcba --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Security.kt @@ -0,0 +1,53 @@ +package app.revanced.api.modules + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import org.koin.ktor.ext.get +import java.util.* +import kotlin.time.Duration.Companion.minutes + +class AuthService( + private val issuer: String, + private val validityInMin: Int, + private val jwtSecret: String, + private val basicUsername: String, + private val basicPassword: String, +) { + val configureSecurity: Application.() -> Unit = { + install(Authentication) { + jwt("jwt") { + verifier( + JWT.require(Algorithm.HMAC256(jwtSecret)) + .withIssuer(issuer) + .build() + ) + validate { credential -> JWTPrincipal(credential.payload) } + } + + basic("basic") { + validate { credentials -> + if (credentials.name == basicUsername && credentials.password == basicPassword) { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + } + + fun newToken(): String { + return JWT.create() + .withIssuer(issuer) + .withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds)) + .sign(Algorithm.HMAC256(jwtSecret)) + } +} + +fun Application.configureSecurity() { + val configureSecurity = get().configureSecurity + configureSecurity() +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/plugins/Serialization.kt b/src/main/kotlin/app/revanced/api/modules/Serialization.kt similarity index 87% rename from src/main/kotlin/app/revanced/api/plugins/Serialization.kt rename to src/main/kotlin/app/revanced/api/modules/Serialization.kt index e2cb0d18..ef385585 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/modules/Serialization.kt @@ -1,4 +1,4 @@ -package app.revanced.api.plugins +package app.revanced.api.modules import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* diff --git a/src/main/kotlin/app/revanced/api/plugins/Databases.kt b/src/main/kotlin/app/revanced/api/plugins/Databases.kt deleted file mode 100644 index c023db16..00000000 --- a/src/main/kotlin/app/revanced/api/plugins/Databases.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.revanced.api.plugins - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.jetbrains.exposed.sql.* - -fun Application.configureDatabases() { - val database = Database.connect( - url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", - user = "root", - driver = "org.h2.Driver", - password = "" - ) - val userService = UserService(database) - routing { - // Create user - post("/users") { - val user = call.receive() - val id = userService.create(user) - call.respond(HttpStatusCode.Created, id) - } - // Read user - get("/users/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - val user = userService.read(id) - if (user != null) { - call.respond(HttpStatusCode.OK, user) - } else { - call.respond(HttpStatusCode.NotFound) - } - } - // Update user - put("/users/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - val user = call.receive() - userService.update(id, user) - call.respond(HttpStatusCode.OK) - } - // Delete user - delete("/users/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - userService.delete(id) - call.respond(HttpStatusCode.OK) - } - } -} diff --git a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt deleted file mode 100644 index 7667e3fb..00000000 --- a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.revanced.api.plugins - -import app.revanced.api.schema.APIConfiguration -import app.revanced.api.backend.github.GitHubBackend -import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.source.decodeFromStream -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.server.application.* -import org.koin.dsl.module -import org.koin.ktor.plugin.Koin -import java.io.File - -fun Application.configureDependencies() { - - install(Koin) { - modules( - module { - single { - Dotenv.load() - } - single { - val configFilePath = get().get("CONFIG_FILE_PATH")!! - Toml.decodeFromStream(File(configFilePath).inputStream()) - } - single { - val token = get().get("GITHUB_TOKEN") - GitHubBackend(token) - } - } - ) - } -} - diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt deleted file mode 100644 index 01382077..00000000 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ /dev/null @@ -1,88 +0,0 @@ -package app.revanced.api.plugins - -import app.revanced.api.backend.github.GitHubBackend -import app.revanced.api.schema.* -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.http.content.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import org.koin.ktor.ext.inject - -fun Application.configureRouting() { - val backend by inject() - val configuration by inject() - - routing { - route("/v${configuration.apiVersion}") { - route("/patches") { - route("latest") { - get { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrations = configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = (patches.assets + integrations.flatMap { it.assets }).filter { - it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") - }.map { APIAsset(it.downloadUrl) }.toSet() - - val release = APIRelease( - patches.tag, - patches.createdAt, - patches.releaseNote, - assets - ) - - call.respond(release) - } - - get("/version") { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - - val release = APIReleaseVersion(patches.tag) - - call.respond(release) - } - } - } - - get("/contributors") { - val contributors = configuration.contributorsRepositoryNames.map { - async { - APIContributable( - it, - backend.getContributors(configuration.organization, it).map { - APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet() - ) - } - }.awaitAll() - - call.respond(contributors) - } - - get("/members") { - val members = backend.getMembers(configuration.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) - } - - call.respond(members) - } - - route("/ping") { - handle { - call.respond(HttpStatusCode.NoContent) - } - } - - staticResources("/", "/static/api") { - contentType { ContentType.Application.Json } - extensions("json") - } - } - - } -} diff --git a/src/main/kotlin/app/revanced/api/plugins/Security.kt b/src/main/kotlin/app/revanced/api/plugins/Security.kt deleted file mode 100644 index 38bcf7f5..00000000 --- a/src/main/kotlin/app/revanced/api/plugins/Security.kt +++ /dev/null @@ -1,30 +0,0 @@ -package app.revanced.api.plugins - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.auth.jwt.* - -fun Application.configureSecurity() { - // Please read the jwt property from the config file if you are using EngineMain - val jwtAudience = "jwt-audience" - val jwtDomain = "https://jwt-provider-domain/" - val jwtRealm = "ktor sample app" - val jwtSecret = "secret" - authentication { - jwt { - realm = jwtRealm - verifier( - JWT - .require(Algorithm.HMAC256(jwtSecret)) - .withAudience(jwtAudience) - .withIssuer(jwtDomain) - .build() - ) - validate { credential -> - if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt b/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt deleted file mode 100644 index 98679974..00000000 --- a/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt +++ /dev/null @@ -1,59 +0,0 @@ -package app.revanced.api.plugins - -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import kotlinx.serialization.Serializable -import kotlinx.coroutines.Dispatchers -import org.jetbrains.exposed.sql.* - -@Serializable -data class ExposedUser(val name: String, val age: Int) -class UserService(private val database: Database) { - object Users : Table() { - val id = integer("id").autoIncrement() - val name = varchar("name", length = 50) - val age = integer("age") - - override val primaryKey = PrimaryKey(id) - } - - init { - transaction(database) { - SchemaUtils.create(Users) - } - } - - suspend fun dbQuery(block: suspend () -> T): T = - newSuspendedTransaction(Dispatchers.IO) { block() } - - suspend fun create(user: ExposedUser): Int = dbQuery { - Users.insert { - it[name] = user.name - it[age] = user.age - }[Users.id] - } - - suspend fun read(id: Int): ExposedUser? { - return dbQuery { - Users.select { Users.id eq id } - .map { ExposedUser(it[Users.name], it[Users.age]) } - .singleOrNull() - } - } - - suspend fun update(id: Int, user: ExposedUser) { - dbQuery { - Users.update({ Users.id eq id }) { - it[name] = user.name - it[age] = user.age - } - } - } - - suspend fun delete(id: Int) { - dbQuery { - Users.deleteWhere { Users.id.eq(id) } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/schema/APISchema.kt index 44324705..295278a4 100644 --- a/src/main/kotlin/app/revanced/api/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/APISchema.kt @@ -1,11 +1,12 @@ package app.revanced.api.schema +import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable @Serializable class APIRelease( val version: String, - val createdAt: String, + val createdAt: LocalDateTime, val changelog: String, val assets: Set ) @@ -56,18 +57,34 @@ class APIReleaseVersion( @Serializable class APIAnnouncement( + val author: String? = null, + val title: String, + val content: String? = null, + val attachmentUrls: Set = emptySet(), + val channel: String? = null, + val archivedAt: LocalDateTime? = null, + val level: Int = 0 +) + +@Serializable +class APIResponseAnnouncement( val id: Int, - val author: APIUser?, + val author: String? = null, val title: String, - val content: APIAnnouncementContent, - val channel: String, - val createdAt: String, - val archivedAt: String?, - val level: Int, + val content: String? = null, + val attachmentUrls: Set = emptySet(), + val channel: String? = null, + val createdAt: LocalDateTime, + val archivedAt: LocalDateTime? = null, + val level: Int = 0 ) @Serializable -class APIAnnouncementContent( - val message: String, - val attachmentUrls: Set +class APILatestAnnouncement( + val id: Int ) + +@Serializable +class APIAnnouncementArchivedAt( + val archivedAt: LocalDateTime +) \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt index c4ba45bc..a38fa2b5 100644 --- a/src/test/kotlin/app/revanced/ApplicationTest.kt +++ b/src/test/kotlin/app/revanced/ApplicationTest.kt @@ -1,6 +1,6 @@ package app.revanced -import app.revanced.api.plugins.configureRouting +import app.revanced.api.modules.configureRouting import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* From 1dba4923c80cd113942dfc3c63aff81251102701 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 31 Jan 2024 09:20:28 +0100 Subject: [PATCH 10/81] chore: Remove test --- src/main/kotlin/app/revanced/api/modules/Routing.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt index 18da3811..b5fdfaf0 100644 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -38,16 +38,6 @@ fun Application.configureRouting() { suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) = block(call.parameters["channel"]!!) - announcementService.new( - APIAnnouncement( - "author", - "title", - "content", - setOf("https://example.com"), - "channel", - LocalDateTime.now().toKotlinLocalDateTime(), - ) - ) route("/{channel}/latest") { get("/id") { channel { announcementId { latestId(it) } } } From 673efc803869dd686eb72982d99d824e0eec4a83 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 31 Jan 2024 11:58:52 +0100 Subject: [PATCH 11/81] test: Add initial test for token --- build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + .../app/revanced/api/modules/Dependencies.kt | 20 ++++---- .../kotlin/app/revanced/ApplicationTest.kt | 48 ++++++++++++++++--- 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b932b6f6..dfab2d57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(libs.picocli) implementation(libs.kotlinx.datetime) + testImplementation(libs.mockk) testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5d36559..c182bba5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ ktor = "2.3.7" ktoml = "0.5.1" picocli = "4.7.3" datetime = "0.5.0" +mockk = "1.13.9" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -41,6 +42,7 @@ ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt index 48ee6c78..80181a7f 100644 --- a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt @@ -29,14 +29,14 @@ val globalModule = module { Dotenv.load() } single { - val configFilePath = get().get("CONFIG_FILE_PATH")!! + val configFilePath = get()["CONFIG_FILE_PATH"] Toml.decodeFromStream(File(configFilePath).inputStream()) } } val gitHubBackendModule = module { single { - val token = get().get("GITHUB_TOKEN") + val token = get()["GITHUB_TOKEN"] GitHubBackend(token) } bind Backend::class } @@ -46,9 +46,9 @@ val databaseModule = module { val dotenv = get() Database.connect( - url = dotenv.get("DB_URL"), - user = dotenv.get("DB_USER"), - password = dotenv.get("DB_PASSWORD"), + url = dotenv["DB_URL"], + user = dotenv["DB_USER"], + password = dotenv["DB_PASSWORD"], driver = "org.h2.Driver" ) } @@ -61,12 +61,12 @@ val authModule = module { single { val dotenv = get() - val jwtSecret = dotenv.get("JWT_SECRET")!! - val issuer = dotenv.get("JWT_ISSUER")!! - val validityInMin = dotenv.get("JWT_VALIDITY_IN_MIN")!!.toInt() + val jwtSecret = dotenv["JWT_SECRET"] + val issuer = dotenv["JWT_ISSUER"] + val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt() - val basicUsername = dotenv.get("BASIC_USERNAME")!! - val basicPassword = dotenv.get("BASIC_PASSWORD")!! + val basicUsername = dotenv["BASIC_USERNAME"] + val basicPassword = dotenv["BASIC_PASSWORD"] AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) } diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt index a38fa2b5..51682fea 100644 --- a/src/test/kotlin/app/revanced/ApplicationTest.kt +++ b/src/test/kotlin/app/revanced/ApplicationTest.kt @@ -1,21 +1,57 @@ package app.revanced -import app.revanced.api.modules.configureRouting +import app.revanced.api.modules.* +import app.revanced.api.schema.APIConfiguration +import com.akuleshov7.ktoml.Toml +import io.github.cdimascio.dotenv.Dotenv import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* +import io.ktor.util.* +import io.mockk.every +import io.mockk.mockk +import kotlinx.serialization.encodeToString import kotlin.test.* class ApplicationTest { @Test - fun testRoot() = testApplication { + fun `successfully create a token`() = testApplication { + val apiConfigurationFile = kotlin.io.path.createTempFile().toFile().apply { + Toml.encodeToString( + APIConfiguration( + organization = "ReVanced", + patchesRepository = "", + integrationsRepositoryNames = setOf(), + contributorsRepositoryNames = setOf() + ) + ).let(::writeText) + + deleteOnExit() + } + + val dotenv = mockk() + every { dotenv[any()] } returns "ReVanced" + every { dotenv["JWT_VALIDITY_IN_MIN"] } returns "5" + every { dotenv["CONFIG_FILE_PATH"] } returns apiConfigurationFile.absolutePath + application { + configureDependencies() + configureHTTP() + configureSerialization() + configureSecurity() configureRouting() } - client.get("/").apply { - assertEquals(HttpStatusCode.OK, status) - assertEquals("Hello World!", bodyAsText()) - } + + val token = client.get("/v1/token") { + headers { + append( + HttpHeaders.Authorization, + "Basic ${"${dotenv["BASIC_USERNAME"]}:${dotenv["BASIC_PASSWORD"]}".encodeBase64()}" + ) + } + }.bodyAsText() + + assert(token.isNotEmpty()) } } From e373d269982428357bd04b659d9d17110cf0d5d2 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 31 Jan 2024 22:27:31 +0100 Subject: [PATCH 12/81] feat: Load system properties --- src/main/kotlin/app/revanced/api/modules/Dependencies.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt index 80181a7f..4f0b14ff 100644 --- a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt @@ -26,7 +26,10 @@ fun Application.configureDependencies() { val globalModule = module { single { - Dotenv.load() + Dotenv.configure() + .ignoreIfMissing() + .systemProperties() + .load() } single { val configFilePath = get()["CONFIG_FILE_PATH"] From 1dccfd2deff3c5de6a6cf2156cac8516b40bdd22 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 1 Feb 2024 03:52:44 +0100 Subject: [PATCH 13/81] fix: Serialize response correctly --- .../app/revanced/api/backend/github/GitHubBackend.kt | 8 ++++---- .../app/revanced/api/backend/github/api/ResponseSchema.kt | 5 ++--- src/main/kotlin/app/revanced/api/modules/Routing.kt | 2 -- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt index 10dcfdaa..49d4c327 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -23,6 +23,8 @@ import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMem import io.ktor.client.plugins.resources.Resources import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy @@ -71,11 +73,9 @@ class GitHubBackend(token: String? = null) : Backend({ return BackendRelease( tag = release.tagName, releaseNote = release.body, - createdAt = release.createdAt, + createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), assets = release.assets.map { - BackendAsset( - downloadUrl = it.browserDownloadUrl - ) + BackendAsset(downloadUrl = it.browserDownloadUrl) }.toSet() ) } diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt index 9fe86ab5..fbfd89aa 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt @@ -1,7 +1,6 @@ package app.revanced.api.backend.github.api -import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.SerialName +import kotlinx.datetime.Instant import kotlinx.serialization.Serializable @@ -41,7 +40,7 @@ class Response { class GitHubRelease( val tagName: String, val assets: Set, - val createdAt: LocalDateTime, + val createdAt: Instant, val body: String ) { @Serializable diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt index b5fdfaf0..f39d7e79 100644 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -12,8 +12,6 @@ import io.ktor.server.routing.* import io.ktor.util.pipeline.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.datetime.toKotlinLocalDateTime -import java.time.LocalDateTime import org.koin.ktor.ext.get as koinGet fun Application.configureRouting() { From c736a75d92d97124e1aca392f37049449a786c84 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 1 Feb 2024 03:54:04 +0100 Subject: [PATCH 14/81] feat: Setup CI/CD --- .env.example | 4 +- .gitattributes | 9 + .github/workflows/pull_request.yml | 26 + .github/workflows/release.yml | 89 + .gitignore | 4 +- .releaserc | 49 + Dockerfile | 31 + LICENSE | 674 +++ build.gradle.kts | 33 +- docker-compose.yml | 23 + gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- package-lock.json | 6895 ++++++++++++++++++++++++++++ package.json | 9 + settings.gradle.kts | 4 +- 15 files changed, 7841 insertions(+), 13 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/release.yml create mode 100644 .releaserc create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 docker-compose.yml create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.env.example b/.env.example index 91a98197..e3a8ba04 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -GITHUB_TOKEN= CONFIG_FILE_PATH=configuration.toml +GITHUB_TOKEN= -DB_URL=jdbc:h2:./api.db +DB_URL=jdbc:h2:./persistence/revanced-api DB_USER= DB_PASSWORD= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..097f9f98 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..41e45541 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,26 @@ +name: Open a PR to main + +on: + push: + branches: + - dev + workflow_dispatch: + +env: + MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main` + +jobs: + pull-request: + name: Open pull request + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Open pull request + uses: repo-sync/pull-request@v2 + with: + destination_branch: 'main' + pr_title: 'chore: ${{ env.MESSAGE }}' + pr_body: 'This pull request will ${{ env.MESSAGE }}.' + pr_draft: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..da4fb63b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: Release + +on: + workflow_dispatch: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Make sure the release step uses its own credentials: + # https://github.com/cycjimmy/semantic-release-action#private-packages + persist-credentials: false + fetch-depth: 0 + + - name: Cache Gradle + uses: burrunan/gradle-cache-action@v1 + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew build clean + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} + run: npm exec semantic-release + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + image: tonistiigi/binfmt:latest + platforms: all + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GH_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: metadata + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: | + latest=${{ startsWith(github.ref, 'refs/heads/main') }} + suffix=-${{ github.sha }} + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v5 + with: + build-args: GH_TOKEN=${{ secrets.GH_TOKEN }} + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64/v8 + cache-to: type=gha,mode=max,ignore-error=true + cache-from: type=gha + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} diff --git a/.gitignore b/.gitignore index dd55c736..a300aea9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ out/ ### Project ### .env -configuration.toml \ No newline at end of file +persistence/ +configuration.toml +.db \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 00000000..79200dc0 --- /dev/null +++ b/.releaserc @@ -0,0 +1,49 @@ +{ + "branches": [ + "main", + { + "name": "dev", + "prerelease": true + } + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", { + "releaseRules": [ + { "type": "build", "scope": "Needs bump", "release": "patch" } + ] + } + ], + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + "gradle-semantic-release-plugin", + [ + "@semantic-release/git", + { + "assets": [ + "README.md", + "CHANGELOG.md", + "gradle.properties", + ] + } + ], + [ + "@semantic-release/github", + { + "assets": [ + { + "path": "build/libs/*.jar" + } + ], + successComment: false + } + ], + [ + "@saithodev/semantic-release-backmerge", + { + backmergeBranches: [{"from": "main", "to": "dev"}], + clearWorkspace: true + } + ] + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5a0c2a19 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM azul/zulu-openjdk:latest + +ARG CONFIG_FILE_PATH + +ARG DB_URL +ARG DB_USER +ARG DB_PASSWORD + +ARG JWT_SECRET +ARG JWT_ISSUER +ARG JWT_VALIDITY_IN_MIN + +ARG BASIC_USERNAME +ARG BASIC_PASSWORD + +ENV CONFIG_FILE_PATH $CONFIG_FILE_PATH + +ENV DB_URL $DB_URL +ENV DB_USER $DB_USER +ENV DB_PASSWORD $DB_PASSWORD + +ENV JWT_SECRET $JWT_SECRET +ENV JWT_ISSUER $JWT_ISSUER +ENV JWT_VALIDITY_IN_MIN $JWT_VALIDITY_IN_MIN + +ENV BASIC_USERNAME $BASIC_USERNAME +ENV BASIC_PASSWORD $BASIC_PASSWORD + +COPY build/libs/revanced-api-*.jar revanced-api.jar + +CMD java -jar revanced-api.jar start \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/build.gradle.kts b/build.gradle.kts index dfab2d57..cb4f866b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,16 +6,37 @@ plugins { group = "app.revanced" +tasks { + processResources { + expand("projectVersion" to project.version) + } + + /* + Dummy task to hack gradle-semantic-release-plugin to release this project. + + Explanation: + SemVer is a standard for versioning libraries. + For that reason the semantic-release plugin uses the "publish" task to publish libraries. + However, this subproject is not a library, and the "publish" task is not available for this subproject. + Because semantic-release is not designed to handle this case, we need to hack it. + + RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 + */ + + register("publish") { + group = "publishing" + description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" + dependsOn(startShadowScripts) + } +} + application { mainClass.set("app.revanced.api.command.MainCommandKt") - - val isDevelopment: Boolean = project.ext.has("development") - applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") } -tasks { - processResources { - expand("projectVersion" to project.version) +ktor { + fatJar { + archiveFileName.set("${project.name}-${project.version}.jar") } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..78d2377c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.8" + +services: + revanced-api: + container_name: revanced-api + image: ghcr.io/revanced/revanced-api:latest + volumes: + - /data/revanced-api/persistence:/persistence + - /data/revanced-api/configuration.toml:/configuration.toml + environment: + - CONFIG_FILE_PATH=configuration.toml + - GITHUB_TOKEN= + - DB_URL=jdbc:h2:./persistence/revanced-api + - DB_USER= + - DB_PASSWORD= + - JWT_SECRET= + - JWT_ISSUER= + - JWT_VALIDITY_IN_MIN=5 + - BASIC_USERNAME= + - BASIC_PASSWORD= + ports: + - 127.0.0.1:8080:8000 + restart: unless-stopped diff --git a/gradle.properties b/gradle.properties index 859e808d..65e2d640 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 0.0.1 \ No newline at end of file +version = 1.0.0 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c182bba5..6bce316e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,4 +47,4 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktor = { id = "io.ktor.plugin", version.ref = "ktor" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..29e79ed2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6895 @@ +{ + "name": "revanced-patches", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@saithodev/semantic-release-backmerge": "^4.0.1", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "gradle-semantic-release-plugin": "^1.9.1", + "semantic-release": "^23.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "dev": true, + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.4.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dev": true, + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz", + "integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", + "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", + "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^19.1.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", + "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@saithodev/semantic-release-backmerge": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-4.0.1.tgz", + "integrity": "sha512-WDsU28YrXSLx0xny7FgFlEk8DCKGcj6OOhA+4Q9k3te1jJD1GZuqY8sbIkVQaw9cqJ7CT+fCZUN6QDad8JW4Dg==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.1.0", + "debug": "^4.3.4", + "execa": "^5.1.1", + "lodash": "^4.17.21", + "semantic-release": "^22.0.7" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/env-ci": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-10.0.0.tgz", + "integrity": "sha512-U4xcd/utDYFgMh0yWj07R1H6L5fwhVbmxBCpnL0DbVSDZVnsC82HONw0wxtxNkIAcua3KtbomQvIk5xFZGAQJw==", + "dev": true, + "dependencies": { + "execa": "^8.0.0", + "java-properties": "^1.0.2" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/env-ci/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/env-ci/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/semantic-release": { + "version": "22.0.12", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", + "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", + "dev": true, + "dependencies": { + "@semantic-release/commit-analyzer": "^11.0.0", + "@semantic-release/error": "^4.0.0", + "@semantic-release/github": "^9.0.0", + "@semantic-release/npm": "^11.0.0", + "@semantic-release/release-notes-generator": "^12.0.0", + "aggregate-error": "^5.0.0", + "cosmiconfig": "^8.0.0", + "debug": "^4.0.0", + "env-ci": "^10.0.0", + "execa": "^8.0.0", + "figures": "^6.0.0", + "find-versions": "^5.1.0", + "get-stream": "^6.0.0", + "git-log-parser": "^1.2.0", + "hook-std": "^3.0.0", + "hosted-git-info": "^7.0.0", + "import-from-esm": "^1.3.1", + "lodash-es": "^4.17.21", + "marked": "^9.0.0", + "marked-terminal": "^6.0.0", + "micromatch": "^4.0.2", + "p-each-series": "^3.0.0", + "p-reduce": "^3.0.0", + "read-pkg-up": "^11.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.2", + "semver-diff": "^4.0.0", + "signale": "^1.2.1", + "yargs": "^17.5.1" + }, + "bin": { + "semantic-release": "bin/semantic-release.js" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/semantic-release/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/semantic-release/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/semantic-release/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/semantic-release/node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/changelog": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", + "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "fs-extra": "^11.0.0", + "lodash": "^4.17.4" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/commit-analyzer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-11.1.0.tgz", + "integrity": "sha512-cXNTbv3nXR2hlzHjAMgbuiQVtvWHTlwwISt60B+4NZv01y/QRY7p2HcJm8Eh2StzcTJoNnflvKjHH/cjFS7d5g==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-filter": "^4.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.0.0", + "import-from-esm": "^1.0.3", + "lodash-es": "^4.17.21", + "micromatch": "^4.0.2" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "dev": true, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@semantic-release/git": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", + "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "dir-glob": "^3.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.4", + "micromatch": "^4.0.0", + "p-reduce": "^2.0.0" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/github": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.2.6.tgz", + "integrity": "sha512-shi+Lrf6exeNZF+sBhK+P011LSbhmIAoUEgEY6SsxF8irJ+J2stwI5jkyDQ+4gzYyDImzV6LCKdYB9FXnQRWKA==", + "dev": true, + "dependencies": { + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "debug": "^4.3.4", + "dir-glob": "^3.0.1", + "globby": "^14.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "issue-parser": "^6.0.0", + "lodash-es": "^4.17.21", + "mime": "^4.0.0", + "p-filter": "^4.0.0", + "url-join": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/github/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/github/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-11.0.2.tgz", + "integrity": "sha512-owtf3RjyPvRE63iUKZ5/xO4uqjRpVQDUB9+nnXj0xwfIeM9pRl+cG+zGDzdftR4m3f2s4Wyf3SexW+kF5DFtWA==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "execa": "^8.0.0", + "fs-extra": "^11.0.0", + "lodash-es": "^4.17.21", + "nerf-dart": "^1.0.0", + "normalize-url": "^8.0.0", + "npm": "^10.0.0", + "rc": "^1.2.8", + "read-pkg": "^9.0.0", + "registry-auth-token": "^5.0.0", + "semver": "^7.1.2", + "tempy": "^3.0.0" + }, + "engines": { + "node": "^18.17 || >=20" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/npm/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@semantic-release/npm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@semantic-release/npm/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-12.1.0.tgz", + "integrity": "sha512-g6M9AjUKAZUZnxaJZnouNBeDNTCUrJ5Ltj+VJ60gJeDaRRahcHsry9HW8yKrnKkKNkx5lbWiEP1FPMqVNQz8Kg==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^7.0.0", + "conventional-changelog-writer": "^7.0.0", + "conventional-commits-filter": "^4.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.0.0", + "get-stream": "^7.0.0", + "import-from-esm": "^1.0.3", + "into-stream": "^7.0.0", + "lodash-es": "^4.17.21", + "read-pkg-up": "^11.0.0" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", + "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", + "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/argv-formatter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", + "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", + "dev": true + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dev": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-7.0.1.tgz", + "integrity": "sha512-Uo+R9neH3r/foIvQ0MKcsXkX642hdm9odUp7TqgFS7BsalTcjzRlIfWZrZR1gbxOozKucaKt5KAbjW8J8xRSmA==", + "dev": true, + "dependencies": { + "conventional-commits-filter": "^4.0.0", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "meow": "^12.0.1", + "semver": "^7.5.2", + "split2": "^4.0.0" + }, + "bin": { + "conventional-changelog-writer": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-filter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-4.0.0.tgz", + "integrity": "sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true + }, + "node_modules/env-ci": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz", + "integrity": "sha512-apikxMgkipkgTvMdRT9MNqWx5VLOci79F4VBd7Op/7OPjjoanjdAvn6fglMCCEf/1bAh8eOiuEVCUs4V3qP3nQ==", + "dev": true, + "dependencies": { + "execa": "^8.0.0", + "java-properties": "^1.0.2" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, + "node_modules/env-ci/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/env-ci/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/env-ci/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/env-ci/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.0.1.tgz", + "integrity": "sha512-0oY/olScYD4IhQ8u//gCPA4F3mlTn2dacYmiDm/mbDQvpmLjV4uH+zhsQ5IyXRyvqkvtUkXkNdGvg5OFJTCsuQ==", + "dev": true, + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-log-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", + "integrity": "sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==", + "dev": true, + "dependencies": { + "argv-formatter": "~1.0.0", + "spawn-error-forwarder": "~1.0.0", + "split2": "~1.0.0", + "stream-combiner2": "~1.1.1", + "through2": "~2.0.0", + "traverse": "~0.6.6" + } + }, + "node_modules/git-log-parser/node_modules/split2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", + "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", + "dev": true, + "dependencies": { + "through2": "~2.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", + "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^1.0.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/gradle-semantic-release-plugin": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/gradle-semantic-release-plugin/-/gradle-semantic-release-plugin-1.9.1.tgz", + "integrity": "sha512-lCrw22itszP/FLSL3N61E40vH1+CU95/4LG9ZF+Fxr8tcx7EPthh2eqVPAq67udFlM8ZgO2LETnn8LSDRq1J2w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/KengoTODA" + } + ], + "dependencies": { + "promisified-properties": "^3.0.0", + "split2": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": "^23.0.0" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hook-std": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", + "integrity": "sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from-esm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.3.tgz", + "integrity": "sha512-U3Qt/CyfFpTUv6LOP2jRTLYjphH6zg3okMfHbyqRa/W2w6hr8OsJWVggNlR4jxuojQy81TgTJTxgSkyoteRGMQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=16.20" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/index-to-position": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", + "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/into-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", + "integrity": "sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==", + "dev": true, + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz", + "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/issue-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", + "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", + "dev": true, + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/java-properties": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", + "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/marked": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.1.1.tgz", + "integrity": "sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-terminal": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-6.2.0.tgz", + "integrity": "sha512-ubWhwcBFHnXsjYNsu+Wndpg0zhY4CahSpPlA70PlO0rR9r2sZpkyU+rkCsOWH+KMEkx847UpALON+HWgxowFtw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cardinal": "^2.1.1", + "chalk": "^5.3.0", + "cli-table3": "^0.6.3", + "node-emoji": "^2.1.3", + "supports-hyperlinks": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <12" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz", + "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nerf-dart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", + "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", + "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.4.0.tgz", + "integrity": "sha512-RS7Mx0OVfXlOcQLRePuDIYdFCVBPCNapWHplDK+mh7GDdP/Tvor4ocuybRRPSvfcRb2vjRJt1fHCqw3cr8qACQ==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^7.2.1", + "@npmcli/config": "^8.0.2", + "@npmcli/fs": "^3.1.0", + "@npmcli/map-workspaces": "^3.0.4", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.1", + "@npmcli/run-script": "^7.0.4", + "@sigstore/tuf": "^2.3.0", + "abbrev": "^2.0.0", + "archy": "~1.0.0", + "cacache": "^18.0.2", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.3", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^7.0.1", + "ini": "^4.1.1", + "init-package-json": "^6.0.0", + "is-cidr": "^5.0.3", + "json-parse-even-better-errors": "^3.0.1", + "libnpmaccess": "^8.0.1", + "libnpmdiff": "^6.0.3", + "libnpmexec": "^7.0.4", + "libnpmfund": "^5.0.1", + "libnpmhook": "^10.0.0", + "libnpmorg": "^6.0.1", + "libnpmpack": "^6.0.3", + "libnpmpublish": "^9.0.2", + "libnpmsearch": "^7.0.0", + "libnpmteam": "^6.0.0", + "libnpmversion": "^5.0.1", + "make-fetch-happen": "^13.0.0", + "minimatch": "^9.0.3", + "minipass": "^7.0.4", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^10.0.1", + "nopt": "^7.2.0", + "normalize-package-data": "^6.0.0", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.3.0", + "npm-package-arg": "^11.0.1", + "npm-pick-manifest": "^9.0.0", + "npm-profile": "^9.0.0", + "npm-registry-fetch": "^16.1.0", + "npm-user-validate": "^2.0.0", + "npmlog": "^7.0.1", + "p-map": "^4.0.0", + "pacote": "^17.0.6", + "parse-conflict-json": "^3.0.1", + "proc-log": "^3.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^2.1.0", + "semver": "^7.5.4", + "spdx-expression-parse": "^3.0.1", + "ssri": "^10.0.5", + "supports-color": "^9.4.0", + "tar": "^6.2.0", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^5.0.0", + "which": "^4.0.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "7.3.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/installed-package-contents": "^2.0.2", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^7.0.0", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/query": "^3.0.1", + "@npmcli/run-script": "^7.0.2", + "bin-links": "^4.0.1", + "cacache": "^18.0.0", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^7.0.1", + "json-parse-even-better-errors": "^3.0.0", + "json-stringify-nice": "^1.1.4", + "minimatch": "^9.0.0", + "nopt": "^7.0.0", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^11.0.1", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "npmlog": "^7.0.1", + "pacote": "^17.0.4", + "parse-conflict-json": "^3.0.0", + "proc-log": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.5", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^4.0.0", + "ini": "^4.1.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^18.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^17.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.1", + "tuf-js": "^2.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "18.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/gauge": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^4.0.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.3.10", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hasown": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^11.0.0", + "promzard": "^1.0.0", + "read": "^2.0.0", + "read-package-json": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/ip": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "4.0.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "2.3.6", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^11.0.1", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "6.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1", + "@npmcli/disparity-colors": "^3.0.0", + "@npmcli/installed-package-contents": "^2.0.2", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^9.0.0", + "npm-package-arg": "^11.0.1", + "pacote": "^17.0.4", + "tar": "^6.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "7.0.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1", + "@npmcli/run-script": "^7.0.2", + "ci-info": "^4.0.0", + "npm-package-arg": "^11.0.1", + "npmlog": "^7.0.1", + "pacote": "^17.0.4", + "proc-log": "^3.0.0", + "read": "^2.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "6.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.2.1", + "@npmcli/run-script": "^7.0.2", + "npm-package-arg": "^11.0.1", + "pacote": "^17.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "9.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^6.0.0", + "npm-package-arg": "^11.0.1", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7", + "sigstore": "^2.2.0", + "ssri": "^10.0.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^16.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "5.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.3", + "@npmcli/run-script": "^7.0.2", + "json-parse-even-better-errors": "^3.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "13.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "6.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "11.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "16.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npmlog": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^4.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^5.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "17.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.10.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.0.15", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.5.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1", + "@sigstore/sign": "^2.2.1", + "@sigstore/tuf": "^2.3.0", + "@sigstore/verify": "^0.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.16", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "10.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.0", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-each-series": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", + "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-filter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", + "dev": true, + "dependencies": { + "p-map": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-map": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.1.tgz", + "integrity": "sha512-2wnaR0XL/FDOj+TgpDuRb2KTjLnu3Fma6b1ZUwGY7LcqenMcvP/YFpjpbPKY6WVGsbuJZRuoUz8iPrt8ORnAFw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-reduce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", + "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parsimmon": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", + "integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw==", + "dev": true + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promisified-properties": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/promisified-properties/-/promisified-properties-3.0.0.tgz", + "integrity": "sha512-ARteuBuUpPg/+spsMhcKHvdtOW/q8btyyVYYxxegGgx+7u9ix9at8DjP2KM2t8+4SuI8wBLt+3X876FMQx91yQ==", + "dev": true, + "dependencies": { + "parsimmon": "^1.13.0" + }, + "engines": { + "node": ">=18", + "npm": ">=7.12" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-11.0.0.tgz", + "integrity": "sha512-LOVbvF1Q0SZdjClSefZ0Nz5z8u+tIE7mV5NibzmE9VYmDe9CaBbAVtz1veOSZbofrdsilxuDAYnFenukZVp8/Q==", + "deprecated": "Renamed to read-package-up", + "dev": true, + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.1.tgz", + "integrity": "sha512-7ZnJYTp6uc04uYRISWtiX3DSKB/fxNQT0B5o1OUeCqiQiwF+JC9+rJiZIDrPrNCLLuTqyQmh4VdQqh/ZOkv9MQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", + "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "index-to-position": "^0.1.2", + "type-fest": "^4.7.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.1.tgz", + "integrity": "sha512-7ZnJYTp6uc04uYRISWtiX3DSKB/fxNQT0B5o1OUeCqiQiwF+JC9+rJiZIDrPrNCLLuTqyQmh4VdQqh/ZOkv9MQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dev": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/registry-auth-token": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", + "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "dev": true, + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/semantic-release": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-23.0.0.tgz", + "integrity": "sha512-Jz7jEWO2igTtske112gC4PPE2whCMVrsgxUPG3/SZI7VE357suIUZFlJd1Yu0g2I6RPc2HxNEfUg7KhmDTjwqg==", + "dev": true, + "dependencies": { + "@semantic-release/commit-analyzer": "^11.0.0", + "@semantic-release/error": "^4.0.0", + "@semantic-release/github": "^9.0.0", + "@semantic-release/npm": "^11.0.0", + "@semantic-release/release-notes-generator": "^12.0.0", + "aggregate-error": "^5.0.0", + "cosmiconfig": "^9.0.0", + "debug": "^4.0.0", + "env-ci": "^11.0.0", + "execa": "^8.0.0", + "figures": "^6.0.0", + "find-versions": "^5.1.0", + "get-stream": "^6.0.0", + "git-log-parser": "^1.2.0", + "hook-std": "^3.0.0", + "hosted-git-info": "^7.0.0", + "import-from-esm": "^1.3.1", + "lodash-es": "^4.17.21", + "marked": "^11.0.0", + "marked-terminal": "^6.0.0", + "micromatch": "^4.0.2", + "p-each-series": "^3.0.0", + "p-reduce": "^3.0.0", + "read-pkg-up": "^11.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.2", + "semver-diff": "^4.0.0", + "signale": "^1.2.1", + "yargs": "^17.5.1" + }, + "bin": { + "semantic-release": "bin/semantic-release.js" + }, + "engines": { + "node": ">=20.8.1" + } + }, + "node_modules/semantic-release/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/semantic-release/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/semantic-release/node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/semantic-release/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/semantic-release/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/signale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", + "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", + "dev": true, + "dependencies": { + "chalk": "^2.3.2", + "figures": "^2.0.0", + "pkg-conf": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/signale/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/signale/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/signale/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-error-forwarder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", + "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", + "dev": true, + "dependencies": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/traverse": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", + "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..31010700 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "devDependencies": { + "@saithodev/semantic-release-backmerge": "^4.0.1", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "gradle-semantic-release-plugin": "^1.9.1", + "semantic-release": "^23.0.0" + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 37eb408b..44d1e0df 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,7 @@ -rootProject.name = "app.revanced.revanced-api" +rootProject.name = "revanced-api" buildCache { local { isEnabled = "CI" !in System.getenv() } -} \ No newline at end of file +} From d0461ff242689426e3de88f88014f40fc05eba7d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 1 Feb 2024 04:16:58 +0100 Subject: [PATCH 15/81] build: Fix git permissions --- gradlew | 0 gradlew.bat | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew mode change 100644 => 100755 gradlew.bat diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 From 81ef4241ded71cd71130f459fb923ffa5eeee100 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 1 Feb 2024 04:23:36 +0100 Subject: [PATCH 16/81] ci: Fix build --- .github/workflows/release.yml | 2 +- build.gradle.kts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da4fb63b..80433630 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew build clean + run: ./gradlew startShadowScripts clean - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/build.gradle.kts b/build.gradle.kts index cb4f866b..82436b3f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,6 @@ tasks { RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 */ - register("publish") { group = "publishing" description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" From 280dbc30f607483adad0bde6ab1b016d5da047ba Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 5 Feb 2024 22:52:55 +0100 Subject: [PATCH 17/81] fix: Don't configure server --- src/main/kotlin/app/revanced/api/command/StartAPICommand.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index b4e3cd6e..b1898ef9 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -25,11 +25,7 @@ internal object StartAPICommand : Runnable { private var port: Int = 8080 override fun run() { - embeddedServer(Netty, port, host, configure = { - connectionGroupSize = 1 - workerGroupSize = 1 - callGroupSize = 1 - }) { + embeddedServer(Netty, port, host) { configureDependencies() configureHTTP() configureSerialization() From c89fdd2588299f49cc8250f103bab68df9ccb12b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 5 Feb 2024 23:56:42 +0100 Subject: [PATCH 18/81] docs: Add README & contribution guidelines --- CONTRIBUTING.md | 97 ++++++++++++++++ README.md | 108 ++++++++++++++++++ .../revanced-headline-vertical-dark.svg | 1 + .../revanced-headline-vertical-light.svg | 1 + assets/revanced-logo/revanced-logo.svg | 1 + 5 files changed, 208 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 assets/revanced-headline/revanced-headline-vertical-dark.svg create mode 100644 assets/revanced-headline/revanced-headline-vertical-light.svg create mode 100644 assets/revanced-logo/revanced-logo.svg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b140bcff --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +

+ + + + +
+ + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ +# 👋 Contribution guidelines + +This document describes how to contribute to ReVanced API. + +## 📖 Resources to help you get started + +* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on +* [Issues](https://github.com/ReVanced/revanced-api/issues) are where we keep track of bugs and feature requests + +## 🙏 Submitting a feature request + +Features can be requested by opening an issue using the +[Feature request issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+). + +> **Note** +> Requests can be accepted or rejected at the discretion of maintainers of ReVanced API. +> Good motivation has to be provided for a request to be accepted. + +## 🐞 Submitting a bug report + +If you encounter a bug while using ReVanced API, open an issue using the +[Bug report issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Bug+report&projects=&template=bug-report.yml&title=bug%3A+). + +## 📝 How to contribute + +1. Before contributing, it is recommended to open an issue to discuss your change +with the maintainers of ReVanced API. This will help you determine whether your change is acceptable +and whether it is worth your time to implement it +2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev` +3. Commit your changes +4. Submit a pull request to the `dev` branch of the repository and reference issues +that your pull request closes in the description of your pull request +5. Our team will review your pull request and provide feedback. Once your pull request is approved, +it will be merged into the `dev` branch and will be included in the next release of ReVanced API + +❤️ Thank you for considering contributing to ReVanced API, +ReVanced diff --git a/README.md b/README.md new file mode 100644 index 00000000..5ce803dc --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +

+ + + + +
+ + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ +# 🚀 ReVanced API + +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-api/release.yml) +![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg) + +API server for ReVanced. + +## ❓ About + +ReVanced API is a server that is used as the backend for ReVanced. +ReVanced API powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) with updates and ReVanced Patches, +and acts as a data source for [ReVanced Website](https://github.com/ReVanced/revanced-website). + +## 💪 Features + +Some of the features of ReVanced API include: + +- 📢 **Announcements**: Push announcements grouped by channels +- ℹ️ About: Get more information such as a description, ways to donate and links about the hoster of ReVanced API +- 🧩 Patches: Get the latest updates of ReVanced Patches directly from ReVanced API +- 👥 Contributors: List all contributors involved in the project + +## 🚀 How to get started + +1. Clone the repository +2. Set up the environment variables in a `.env` file using the `.env.example` file as a template +3. Configure the `configuration.toml` file using the `configuration.toml.example` file as a template +4. Run the server using `gradlew run --args=start` + +## 📚 Everything else + +### 📙 Contributing + +Thank you for considering contributing to ReVanced API. You can find the contribution guidelines [here](CONTRIBUTING.md). + +### 🛠️ Building + +In order to build ReVanced API, follow these steps: + +1. Clone the repository +2. Run `gradlew build` to build the project + +## 📜 Licence + +ReVanced API is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information. +[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and +modify ReVanced API as long as you track changes/dates in source files. +Any modifications to ReVanced API must also be made available under the GPL along with build & install instructions. diff --git a/assets/revanced-headline/revanced-headline-vertical-dark.svg b/assets/revanced-headline/revanced-headline-vertical-dark.svg new file mode 100644 index 00000000..a59bfb50 --- /dev/null +++ b/assets/revanced-headline/revanced-headline-vertical-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/revanced-headline/revanced-headline-vertical-light.svg b/assets/revanced-headline/revanced-headline-vertical-light.svg new file mode 100644 index 00000000..3c5eeccc --- /dev/null +++ b/assets/revanced-headline/revanced-headline-vertical-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/revanced-logo/revanced-logo.svg b/assets/revanced-logo/revanced-logo.svg new file mode 100644 index 00000000..901e1914 --- /dev/null +++ b/assets/revanced-logo/revanced-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file From 24c6f4e4354b4e6da0e4a4e7f0ee0a7a5e3c90ed Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 5 Feb 2024 23:57:21 +0100 Subject: [PATCH 19/81] feat: Do not ignore, if `.env` file is missing --- src/main/kotlin/app/revanced/api/modules/Dependencies.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt index 4f0b14ff..39104009 100644 --- a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt @@ -27,7 +27,6 @@ fun Application.configureDependencies() { val globalModule = module { single { Dotenv.configure() - .ignoreIfMissing() .systemProperties() .load() } From 0d19eb7e0f6be9846398a8d3c98a69b47ae72352 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 6 Feb 2024 00:01:11 +0100 Subject: [PATCH 20/81] docs: Fix line-break --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b140bcff..fa880ff2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,5 +93,5 @@ that your pull request closes in the description of your pull request 5. Our team will review your pull request and provide feedback. Once your pull request is approved, it will be merged into the `dev` branch and will be included in the next release of ReVanced API -❤️ Thank you for considering contributing to ReVanced API, +❤️ Thank you for considering contributing to ReVanced API, ReVanced From 906fd26bdb069caa297231ba00b6d6a48c074c85 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 6 Feb 2024 00:01:23 +0100 Subject: [PATCH 21/81] docs: Improve wording --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ce803dc..ba68ed72 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,7 @@ API server for ReVanced. ## ❓ About ReVanced API is a server that is used as the backend for ReVanced. -ReVanced API powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) with updates and ReVanced Patches, -and acts as a data source for [ReVanced Website](https://github.com/ReVanced/revanced-website). +ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) with updates and ReVanced Patches. ## 💪 Features From 6b34aaf5db06638e0b67e7cc58d546acf455c2e0 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 6 Feb 2024 00:03:40 +0100 Subject: [PATCH 22/81] docs: Fix spelling error --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba68ed72..5973b63a 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,10 @@ ReVanced API acts as the data source for [ReVanced Website](https://github.com/R ## 💪 Features -Some of the features of ReVanced API include: +Some of the features ReVanced API include: - 📢 **Announcements**: Push announcements grouped by channels -- ℹ️ About: Get more information such as a description, ways to donate and links about the hoster of ReVanced API +- ℹ️ About: Get more information such as a description, ways to donate, and links about the hoster of ReVanced API - 🧩 Patches: Get the latest updates of ReVanced Patches directly from ReVanced API - 👥 Contributors: List all contributors involved in the project From e798a4c070b313bf2141d00ea1d5b70a73c34866 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 6 Feb 2024 00:21:58 +0100 Subject: [PATCH 23/81] style: Lint code --- .editorconfig | 3 + .../app/revanced/api/backend/Backend.kt | 16 +- .../api/backend/github/GitHubBackend.kt | 48 +++--- .../api/backend/github/api/RequestResource.kt | 1 + .../api/backend/github/api/ResponseSchema.kt | 7 +- .../app/revanced/api/command/MainCommand.kt | 2 +- .../app/revanced/api/modules/Database.kt | 9 +- .../app/revanced/api/modules/Dependencies.kt | 4 +- .../app/revanced/api/modules/Routing.kt | 144 ++++++++++++------ .../app/revanced/api/modules/Security.kt | 4 +- .../app/revanced/api/schema/APISchema.kt | 18 +-- .../api/schema/ConfigurationSchema.kt | 4 +- .../kotlin/app/revanced/ApplicationTest.kt | 6 +- 13 files changed, 158 insertions(+), 108 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..2d6d258f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{kt,kts}] +ktlint_code_style = intellij_idea +ktlint_standard_no-wildcard-imports = disabled \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/backend/Backend.kt index e2319493..c3e53c1c 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/backend/Backend.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.Serializable * @param httpClientConfig The configuration of the HTTP client. */ abstract class Backend( - httpClientConfig: HttpClientConfig.() -> Unit = {} + httpClientConfig: HttpClientConfig.() -> Unit = {}, ) { protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) @@ -34,7 +34,7 @@ abstract class Backend( * @property members The members of the organization. */ class BackendOrganization( - val members: Set + val members: Set, ) { /** * A member of an organization. @@ -46,12 +46,12 @@ abstract class Backend( * @property gpgKeysUrl The URL to the GPG keys of the member. */ @Serializable - class BackendMember ( + class BackendMember( override val name: String, override val avatarUrl: String, override val url: String, val bio: String?, - val gpgKeysUrl: String + val gpgKeysUrl: String, ) : BackendUser /** @@ -60,7 +60,7 @@ abstract class Backend( * @property contributors The contributors of the repository. */ class BackendRepository( - val contributors: Set + val contributors: Set, ) { /** * A contributor of a repository. @@ -75,7 +75,7 @@ abstract class Backend( override val name: String, override val avatarUrl: String, override val url: String, - val contributions: Int + val contributions: Int, ) : BackendUser /** @@ -91,7 +91,7 @@ abstract class Backend( val tag: String, val releaseNote: String, val createdAt: LocalDateTime, - val assets: Set + val assets: Set, ) { /** * An asset of a release. @@ -100,7 +100,7 @@ abstract class Backend( */ @Serializable class BackendAsset( - val downloadUrl: String + val downloadUrl: String, ) } } diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt index 49d4c327..be0db97b 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -1,7 +1,18 @@ package app.revanced.api.backend.github import app.revanced.api.backend.Backend +import app.revanced.api.backend.Backend.BackendOrganization.BackendMember +import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset import app.revanced.api.backend.github.api.Request +import app.revanced.api.backend.github.api.Request.Organization.Members +import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors +import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases +import app.revanced.api.backend.github.api.Response +import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember +import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease import io.ktor.client.call.* import io.ktor.client.plugins.* import io.ktor.client.plugins.auth.* @@ -9,17 +20,6 @@ import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.resources.* -import app.revanced.api.backend.Backend.BackendOrganization.BackendMember -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset -import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases -import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors -import app.revanced.api.backend.github.api.Request.Organization.Members -import app.revanced.api.backend.github.api.Response -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember import io.ktor.client.plugins.resources.Resources import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* @@ -28,18 +28,18 @@ import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy -import org.koin.dsl.bind -import org.koin.dsl.module @OptIn(ExperimentalSerializationApi::class) class GitHubBackend(token: String? = null) : Backend({ install(HttpCache) install(Resources) install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - namingStrategy = JsonNamingStrategy.SnakeCase - }) + json( + Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase + }, + ) } defaultRequest { url("https://api.github.com") } @@ -50,7 +50,7 @@ class GitHubBackend(token: String? = null) : Backend({ loadTokens { BearerTokens( accessToken = it, - refreshToken = "" // Required dummy value + refreshToken = "", // Required dummy value ) } @@ -64,11 +64,11 @@ class GitHubBackend(token: String? = null) : Backend({ repository: String, tag: String?, ): BackendRelease { - val release: GitHubRelease = if (tag != null) + val release: GitHubRelease = if (tag != null) { client.get(Releases.Tag(owner, repository, tag)).body() - else + } else { client.get(Releases.Latest(owner, repository)).body() - + } return BackendRelease( tag = release.tagName, @@ -76,13 +76,13 @@ class GitHubBackend(token: String? = null) : Backend({ createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), assets = release.assets.map { BackendAsset(downloadUrl = it.browserDownloadUrl) - }.toSet() + }.toSet(), ) } override suspend fun getContributors( owner: String, - repository: String + repository: String, ): Set { val contributors: Set = client.get(Contributors(owner, repository)).body() @@ -91,7 +91,7 @@ class GitHubBackend(token: String? = null) : Backend({ name = it.login, avatarUrl = it.avatarUrl, url = it.url, - contributions = it.contributions + contributions = it.contributions, ) }.toSet() } diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt b/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt index ab78199a..dc480ba6 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt @@ -5,6 +5,7 @@ import io.ktor.resources.* class Request { @Resource("/users/{username}") class User(val username: String) + class Organization { @Resource("/orgs/{org}/members") class Members(val org: String) diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt index fbfd89aa..693a8975 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt @@ -3,7 +3,6 @@ package app.revanced.api.backend.github.api import kotlinx.datetime.Instant import kotlinx.serialization.Serializable - class Response { interface IGitHubUser { val login: String @@ -12,7 +11,7 @@ class Response { } @Serializable - class GitHubUser ( + class GitHubUser( override val login: String, override val avatarUrl: String, override val url: String, @@ -41,11 +40,11 @@ class Response { val tagName: String, val assets: Set, val createdAt: Instant, - val body: String + val body: String, ) { @Serializable class GitHubAsset( - val browserDownloadUrl: String + val browserDownloadUrl: String, ) } } diff --git a/src/main/kotlin/app/revanced/api/command/MainCommand.kt b/src/main/kotlin/app/revanced/api/command/MainCommand.kt index 3038e963..6b773879 100644 --- a/src/main/kotlin/app/revanced/api/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/api/command/MainCommand.kt @@ -31,4 +31,4 @@ private object CLIVersionProvider : CommandLine.IVersionProvider { StartAPICommand::class, ], ) -private object MainCommand \ No newline at end of file +private object MainCommand diff --git a/src/main/kotlin/app/revanced/api/modules/Database.kt b/src/main/kotlin/app/revanced/api/modules/Database.kt index fcd09551..4727a0b4 100644 --- a/src/main/kotlin/app/revanced/api/modules/Database.kt +++ b/src/main/kotlin/app/revanced/api/modules/Database.kt @@ -1,11 +1,10 @@ package app.revanced.api.modules import app.revanced.api.modules.AnnouncementService.Attachments.announcement -import app.revanced.api.schema.APIResponseAnnouncement import app.revanced.api.schema.APIAnnouncement import app.revanced.api.schema.APILatestAnnouncement +import app.revanced.api.schema.APIResponseAnnouncement import kotlinx.datetime.* -import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID @@ -13,7 +12,7 @@ import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.kotlin.datetime.datetime - +import org.jetbrains.exposed.sql.transactions.transaction class AnnouncementService(private val database: Database) { private object Announcements : IntIdTable() { @@ -52,7 +51,7 @@ class AnnouncementService(private val database: Database) { channel, createdAt, archivedAt, - level + level, ) } @@ -107,7 +106,7 @@ class AnnouncementService(private val database: Database) { fun archive( id: Int, - archivedAt: LocalDateTime? + archivedAt: LocalDateTime?, ) = transaction { Announcement.findById(id)?.apply { this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt index 39104009..cf8afe07 100644 --- a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt @@ -19,7 +19,7 @@ fun Application.configureDependencies() { globalModule, gitHubBackendModule, databaseModule, - authModule + authModule, ) } } @@ -51,7 +51,7 @@ val databaseModule = module { url = dotenv["DB_URL"], user = dotenv["DB_USER"], password = dotenv["DB_PASSWORD"], - driver = "org.h2.Driver" + driver = "org.h2.Driver", ) } factory { diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt index f39d7e79..846bfc2d 100644 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -23,41 +23,68 @@ fun Application.configureRouting() { routing { route("/v${configuration.apiVersion}") { route("/announcements") { - suspend fun PipelineContext<*, ApplicationCall>.announcement( - block: AnnouncementService.() -> APIResponseAnnouncement? - ) = announcementService.block()?.let { call.respond(it) } - ?: call.respond(HttpStatusCode.NotFound) + suspend fun PipelineContext<*, ApplicationCall>.announcement(block: AnnouncementService.() -> APIResponseAnnouncement?) = + announcementService.block()?.let { call.respond(it) } + ?: call.respond(HttpStatusCode.NotFound) - suspend fun PipelineContext<*, ApplicationCall>.announcementId( - block: AnnouncementService.() -> APILatestAnnouncement? - ) = announcementService.block()?.let { call.respond(it) } - ?: call.respond(HttpStatusCode.NotFound) + suspend fun PipelineContext<*, ApplicationCall>.announcementId(block: AnnouncementService.() -> APILatestAnnouncement?) = + announcementService.block()?.let { call.respond(it) } + ?: call.respond(HttpStatusCode.NotFound) suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) = block(call.parameters["channel"]!!) route("/{channel}/latest") { - get("/id") { channel { announcementId { latestId(it) } } } + get("/id") { + channel { + announcementId { + latestId(it) + } + } + } - get { channel { announcement { latest(it) } } } + get { + channel { + announcement { + latest(it) + } + } + } } - get("/{channel}") { channel { call.respond(announcementService.read(it)) } } + get("/{channel}") { + channel { + call.respond(announcementService.read(it)) + } + } route("/latest") { - get("/id") { announcementId { latestId() } } + get("/id") { + announcementId { + latestId() + } + } - get { announcement { latest() } } + get { + announcement { + latest() + } + } } - get { call.respond(announcementService.read()) } + get { + call.respond(announcementService.read()) + } authenticate("jwt") { suspend fun PipelineContext<*, ApplicationCall>.id(block: suspend (Int) -> Unit) = - call.parameters["id"]!!.toIntOrNull()?.let { block(it) } - ?: call.respond(HttpStatusCode.BadRequest) + call.parameters["id"]!!.toIntOrNull()?.let { + block(it) + } ?: call.respond(HttpStatusCode.BadRequest) - post { announcementService.new(call.receive()) } + post { + announcementService.new(call.receive()) + } post("/{id}/archive") { id { @@ -66,11 +93,23 @@ fun Application.configureRouting() { } } - post("/{id}/unarchive") { id { announcementService.unarchive(it) } } + post("/{id}/unarchive") { + id { + announcementService.unarchive(it) + } + } - patch("/{id}") { id { announcementService.update(it, call.receive()) } } + patch("/{id}") { + id { + announcementService.update(it, call.receive()) + } + } - delete("/{id}") { id { announcementService.delete(it) } } + delete("/{id}") { + id { + announcementService.delete(it) + } + } } } @@ -78,20 +117,23 @@ fun Application.configureRouting() { route("latest") { get { val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrations = configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = (patches.assets + integrations.flatMap { it.assets }).filter { - it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") - }.map { APIAsset(it.downloadUrl) }.toSet() - - val release = APIRelease( - patches.tag, - patches.createdAt, - patches.releaseNote, - assets - ) + val integrations = + configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() + + val assets = + (patches.assets + integrations.flatMap { it.assets }).filter { + it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") + }.map { APIAsset(it.downloadUrl) }.toSet() + + val release = + APIRelease( + patches.tag, + patches.createdAt, + patches.releaseNote, + assets, + ) call.respond(release) } @@ -112,24 +154,26 @@ fun Application.configureRouting() { } get("/contributors") { - val contributors = configuration.contributorsRepositoryNames.map { - async { - APIContributable( - it, - backend.getContributors(configuration.organization, it).map { - APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet() - ) - } - }.awaitAll() + val contributors = + configuration.contributorsRepositoryNames.map { + async { + APIContributable( + it, + backend.getContributors(configuration.organization, it).map { + APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + }.toSet(), + ) + } + }.awaitAll() call.respond(contributors) } get("/team") { - val team = backend.getMembers(configuration.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) - } + val team = + backend.getMembers(configuration.organization).map { + APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + } call.respond(team) } @@ -140,7 +184,11 @@ fun Application.configureRouting() { } } - authenticate("basic") { get("/token") { call.respond(authService.newToken()) } } + authenticate("basic") { + get("/token") { + call.respond(authService.newToken()) + } + } } } } diff --git a/src/main/kotlin/app/revanced/api/modules/Security.kt b/src/main/kotlin/app/revanced/api/modules/Security.kt index 0dd2dcba..02cc5f3c 100644 --- a/src/main/kotlin/app/revanced/api/modules/Security.kt +++ b/src/main/kotlin/app/revanced/api/modules/Security.kt @@ -22,7 +22,7 @@ class AuthService( verifier( JWT.require(Algorithm.HMAC256(jwtSecret)) .withIssuer(issuer) - .build() + .build(), ) validate { credential -> JWTPrincipal(credential.payload) } } @@ -50,4 +50,4 @@ class AuthService( fun Application.configureSecurity() { val configureSecurity = get().configureSecurity configureSecurity() -} \ No newline at end of file +} diff --git a/src/main/kotlin/app/revanced/api/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/schema/APISchema.kt index 295278a4..fe3e2c96 100644 --- a/src/main/kotlin/app/revanced/api/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/APISchema.kt @@ -8,7 +8,7 @@ class APIRelease( val version: String, val createdAt: LocalDateTime, val changelog: String, - val assets: Set + val assets: Set, ) interface APIUser { @@ -22,7 +22,7 @@ class APIMember( override val name: String, override val avatarUrl: String, override val url: String, - val gpgKeysUrl: String + val gpgKeysUrl: String, ) : APIUser @Serializable @@ -36,7 +36,7 @@ class APIContributor( @Serializable class APIContributable( val name: String, - val contributors: Set + val contributors: Set, ) @Serializable @@ -52,7 +52,7 @@ class APIAsset( @Serializable class APIReleaseVersion( - val version: String + val version: String, ) @Serializable @@ -63,7 +63,7 @@ class APIAnnouncement( val attachmentUrls: Set = emptySet(), val channel: String? = null, val archivedAt: LocalDateTime? = null, - val level: Int = 0 + val level: Int = 0, ) @Serializable @@ -76,15 +76,15 @@ class APIResponseAnnouncement( val channel: String? = null, val createdAt: LocalDateTime, val archivedAt: LocalDateTime? = null, - val level: Int = 0 + val level: Int = 0, ) @Serializable class APILatestAnnouncement( - val id: Int + val id: Int, ) @Serializable class APIAnnouncementArchivedAt( - val archivedAt: LocalDateTime -) \ No newline at end of file + val archivedAt: LocalDateTime, +) diff --git a/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt b/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt index db90377a..5eff0b6c 100644 --- a/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt @@ -13,5 +13,5 @@ class APIConfiguration( @SerialName("contributors-repositories") val contributorsRepositoryNames: Set, @SerialName("api-version") - val apiVersion: Int = 1 -) \ No newline at end of file + val apiVersion: Int = 1, +) diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt index 51682fea..5f1ce2e8 100644 --- a/src/test/kotlin/app/revanced/ApplicationTest.kt +++ b/src/test/kotlin/app/revanced/ApplicationTest.kt @@ -23,8 +23,8 @@ class ApplicationTest { organization = "ReVanced", patchesRepository = "", integrationsRepositoryNames = setOf(), - contributorsRepositoryNames = setOf() - ) + contributorsRepositoryNames = setOf(), + ), ).let(::writeText) deleteOnExit() @@ -47,7 +47,7 @@ class ApplicationTest { headers { append( HttpHeaders.Authorization, - "Basic ${"${dotenv["BASIC_USERNAME"]}:${dotenv["BASIC_PASSWORD"]}".encodeBase64()}" + "Basic ${"${dotenv["BASIC_USERNAME"]}:${dotenv["BASIC_PASSWORD"]}".encodeBase64()}", ) } }.bodyAsText() From 6c930fff9a99f9ee23edd3d661694d2074f53b23 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 7 Feb 2024 01:16:35 +0100 Subject: [PATCH 24/81] feat: Add `/list` route --- build.gradle.kts | 8 +- gradle/libs.versions.toml | 6 ++ .../kotlin/app/revanced/api/modules/HTTP.kt | 4 +- .../app/revanced/api/modules/Routing.kt | 87 ++++++++++++++----- .../app/revanced/api/schema/APISchema.kt | 18 +++- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 82436b3f..c9b481ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,7 +21,7 @@ tasks { Because semantic-release is not designed to handle this case, we need to hack it. RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - */ + */ register("publish") { group = "publishing" description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" @@ -41,6 +41,9 @@ ktor { repositories { mavenCentral() + google() + maven { url = uri("https://jitpack.io") } + mavenLocal() } dependencies { @@ -72,6 +75,9 @@ dependencies { implementation(libs.ktoml.file) implementation(libs.picocli) implementation(libs.kotlinx.datetime) + implementation(libs.revanced.patcher) + implementation(libs.revanced.library) + implementation(libs.caffeine) testImplementation(libs.mockk) testImplementation(libs.ktor.server.tests) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bce316e..f00cb0f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,9 @@ ktoml = "0.5.1" picocli = "4.7.3" datetime = "0.5.0" mockk = "1.13.9" +revanced-patcher = "19.2.0" +revanced-library = "1.5.0" +caffeine = "3.1.8" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -43,6 +46,9 @@ ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } +revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } +caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/modules/HTTP.kt b/src/main/kotlin/app/revanced/api/modules/HTTP.kt index 590629ba..1f3248f9 100644 --- a/src/main/kotlin/app/revanced/api/modules/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/modules/HTTP.kt @@ -19,8 +19,6 @@ fun Application.configureHTTP() { anyHost() // @TODO: Don't do this in production if possible. Try to limit it. } install(CachingHeaders) { - options { _, _ -> - CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) - } + options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } } } diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt index 846bfc2d..61ad1067 100644 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -2,6 +2,9 @@ package app.revanced.api.modules import app.revanced.api.backend.Backend import app.revanced.api.schema.* +import app.revanced.library.PatchUtils +import app.revanced.patcher.PatchBundleLoader +import com.github.benmanes.caffeine.cache.Caffeine import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -12,6 +15,8 @@ import io.ktor.server.routing.* import io.ktor.util.pipeline.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import java.io.File +import java.net.URL import org.koin.ktor.ext.get as koinGet fun Application.configureRouting() { @@ -116,34 +121,70 @@ fun Application.configureRouting() { route("/patches") { route("latest") { get { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrations = - configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = - (patches.assets + integrations.flatMap { it.assets }).filter { - it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") - }.map { APIAsset(it.downloadUrl) }.toSet() - - val release = - APIRelease( - patches.tag, - patches.createdAt, - patches.releaseNote, - assets, - ) - - call.respond(release) + val patchesRelease = + backend.getRelease(configuration.organization, configuration.patchesRepository) + val integrationsReleases = configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() + + val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) + .map { APIAsset(it.downloadUrl) } + .filter { it.type != APIAsset.Type.UNKNOWN } + .toSet() + + val apiRelease = APIRelease( + patchesRelease.tag, + patchesRelease.createdAt, + patchesRelease.releaseNote, + assets, + ) + + call.respond(apiRelease) } get("/version") { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + val patchesRelease = + backend.getRelease(configuration.organization, configuration.patchesRepository) - val release = APIReleaseVersion(patches.tag) + val apiPatchesRelease = APIReleaseVersion(patchesRelease.tag) - call.respond(release) + call.respond(apiPatchesRelease) + } + + val fileCache = Caffeine + .newBuilder() + .evictionListener { _, value, _ -> value?.delete() } + .maximumSize(1) + .build() + + get("/list") { + val patchesRelease = + backend.getRelease(configuration.organization, configuration.patchesRepository) + + // Get the cached patches file or download and cache a new one. + // The old file is deleted on eviction. + val patchesFile = fileCache.getIfPresent(patchesRelease.tag) ?: run { + val downloadUrl = patchesRelease.assets + .map { APIAsset(it.downloadUrl) } + .find { it.type == APIAsset.Type.PATCHES } + ?.downloadUrl + + kotlin.io.path.createTempFile().toFile().apply { + outputStream().use { URL(downloadUrl).openStream().copyTo(it) } + }.also { + fileCache.put(patchesRelease.tag, it) + it.deleteOnExit() + } + } + + call.respondOutputStream( + contentType = ContentType.Application.Json, + ) { + PatchUtils.Json.serialize( + PatchBundleLoader.Jar(patchesFile), + outputStream = this, + ) + } } } } diff --git a/src/main/kotlin/app/revanced/api/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/schema/APISchema.kt index fe3e2c96..ec6355b6 100644 --- a/src/main/kotlin/app/revanced/api/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/APISchema.kt @@ -1,6 +1,7 @@ package app.revanced.api.schema import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -44,9 +45,20 @@ class APIAsset( val downloadUrl: String, ) { val type = when { - downloadUrl.endsWith(".jar") -> "patches" - downloadUrl.endsWith(".apk") -> "integrations" - else -> "unknown" + downloadUrl.endsWith(".jar") -> Type.PATCHES + downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS + else -> Type.UNKNOWN + } + + enum class Type { + @SerialName("patches") + PATCHES, + + @SerialName("integrations") + INTEGRATIONS, + + @SerialName("unknown") + UNKNOWN, } } From ee971e3902bb7a04a38be642eab0f5f3e3b503da Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 7 Feb 2024 16:46:26 +0100 Subject: [PATCH 25/81] docs: Fix wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5973b63a..dcdfb16f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ ReVanced API acts as the data source for [ReVanced Website](https://github.com/R Some of the features ReVanced API include: - 📢 **Announcements**: Push announcements grouped by channels -- ℹ️ About: Get more information such as a description, ways to donate, and links about the hoster of ReVanced API +- ℹ️ About: Get more information such as a description about, ways to donate to, and links of the hoster of ReVanced API - 🧩 Patches: Get the latest updates of ReVanced Patches directly from ReVanced API - 👥 Contributors: List all contributors involved in the project From bb0b78df8ace5993cbbb11583c4c6baa404b4a0c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 7 Feb 2024 16:47:52 +0100 Subject: [PATCH 26/81] docs: Make all feature titles bold --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dcdfb16f..91ba5df4 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,9 @@ ReVanced API acts as the data source for [ReVanced Website](https://github.com/R Some of the features ReVanced API include: - 📢 **Announcements**: Push announcements grouped by channels -- ℹ️ About: Get more information such as a description about, ways to donate to, and links of the hoster of ReVanced API -- 🧩 Patches: Get the latest updates of ReVanced Patches directly from ReVanced API -- 👥 Contributors: List all contributors involved in the project +- ℹ️ **About**: Get more information such as a description about, ways to donate to, and links of the hoster of ReVanced API +- 🧩 **Patches**: Get the latest updates of ReVanced Patches directly from ReVanced API +- 👥 **Contributors**: List all contributors involved in the project ## 🚀 How to get started From 7a1957d013cfd0851dc0191b715e8f36c530d9d2 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 8 Feb 2024 03:56:16 +0100 Subject: [PATCH 27/81] perf: Cache patches list instead of just the patches file --- .../app/revanced/api/modules/Routing.kt | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt index 61ad1067..616e2e5e 100644 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -15,7 +15,7 @@ import io.ktor.server.routing.* import io.ktor.util.pipeline.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import java.io.File +import java.io.ByteArrayOutputStream import java.net.URL import org.koin.ktor.ext.get as koinGet @@ -151,40 +151,37 @@ fun Application.configureRouting() { call.respond(apiPatchesRelease) } - val fileCache = Caffeine + val patchesListCache = Caffeine .newBuilder() - .evictionListener { _, value, _ -> value?.delete() } .maximumSize(1) - .build() + .build() get("/list") { val patchesRelease = backend.getRelease(configuration.organization, configuration.patchesRepository) - // Get the cached patches file or download and cache a new one. - // The old file is deleted on eviction. - val patchesFile = fileCache.getIfPresent(patchesRelease.tag) ?: run { + val patchesListByteArray = patchesListCache.getIfPresent(patchesRelease.tag) ?: run { val downloadUrl = patchesRelease.assets .map { APIAsset(it.downloadUrl) } .find { it.type == APIAsset.Type.PATCHES } ?.downloadUrl - kotlin.io.path.createTempFile().toFile().apply { + val patches = kotlin.io.path.createTempFile().toFile().apply { outputStream().use { URL(downloadUrl).openStream().copyTo(it) } + }.let { file -> + PatchBundleLoader.Jar(file).also { file.delete() } + } + + ByteArrayOutputStream().use { stream -> + PatchUtils.Json.serialize(patches, outputStream = stream) + + stream.toByteArray() }.also { - fileCache.put(patchesRelease.tag, it) - it.deleteOnExit() + patchesListCache.put(patchesRelease.tag, it) } } - call.respondOutputStream( - contentType = ContentType.Application.Json, - ) { - PatchUtils.Json.serialize( - PatchBundleLoader.Jar(patchesFile), - outputStream = this, - ) - } + call.respondBytes(ContentType.Application.Json) { patchesListByteArray } } } } From fa2f8b2f86d2eb5b11196ba868fb95b2b6335a94 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 03:07:28 +0200 Subject: [PATCH 28/81] refactor: Refactor into services and repositories --- build.gradle.kts | 29 +-- gradle/libs.versions.toml | 12 +- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 41 +++- gradlew.bat | 35 +-- .../revanced/api/command/StartAPICommand.kt | 5 +- .../api/configuration/Dependencies.kt | 83 +++++++ .../api/{modules => configuration}/HTTP.kt | 8 +- .../revanced/api/configuration/Security.kt | 9 + .../api/configuration/Serialization.kt | 19 ++ .../api/configuration/routing/Routing.kt | 19 ++ .../routing/routes/Announcements.kt | 86 +++++++ .../configuration/routing/routes/ApiRoute.kt | 41 ++++ .../routing/routes/PatchesRoute.kt | 26 ++ .../app/revanced/api/modules/Dependencies.kt | 75 ------ .../app/revanced/api/modules/Routing.kt | 232 ------------------ .../app/revanced/api/modules/Serialization.kt | 11 - .../AnnouncementRepository.kt} | 151 ++++++------ .../ConfigurationRepository.kt} | 4 +- .../backend/BackendRepository.kt} | 10 +- .../github/GitHubBackendRepository.kt} | 36 +-- .../backend/github/api/Request.kt} | 2 +- .../backend/github/api/Response.kt} | 2 +- .../api/services/AnnouncementService.kt | 35 +++ .../app/revanced/api/services/ApiService.kt | 33 +++ .../Security.kt => services/AuthService.kt} | 10 +- .../revanced/api/services/PatchesService.kt | 87 +++++++ src/main/resources/static/api/about.json | 2 +- .../kotlin/app/revanced/ApplicationTest.kt | 57 ----- 30 files changed, 623 insertions(+), 542 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/configuration/Dependencies.kt rename src/main/kotlin/app/revanced/api/{modules => configuration}/HTTP.kt (81%) create mode 100644 src/main/kotlin/app/revanced/api/configuration/Security.kt create mode 100644 src/main/kotlin/app/revanced/api/configuration/Serialization.kt create mode 100644 src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt create mode 100644 src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt create mode 100644 src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt create mode 100644 src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt delete mode 100644 src/main/kotlin/app/revanced/api/modules/Dependencies.kt delete mode 100644 src/main/kotlin/app/revanced/api/modules/Routing.kt delete mode 100644 src/main/kotlin/app/revanced/api/modules/Serialization.kt rename src/main/kotlin/app/revanced/api/{modules/Database.kt => repository/AnnouncementRepository.kt} (52%) rename src/main/kotlin/app/revanced/api/{schema/ConfigurationSchema.kt => repository/ConfigurationRepository.kt} (85%) rename src/main/kotlin/app/revanced/api/{backend/Backend.kt => repository/backend/BackendRepository.kt} (91%) rename src/main/kotlin/app/revanced/api/{backend/github/GitHubBackend.kt => repository/backend/github/GitHubBackendRepository.kt} (67%) rename src/main/kotlin/app/revanced/api/{backend/github/api/RequestResource.kt => repository/backend/github/api/Request.kt} (93%) rename src/main/kotlin/app/revanced/api/{backend/github/api/ResponseSchema.kt => repository/backend/github/api/Response.kt} (96%) create mode 100644 src/main/kotlin/app/revanced/api/services/AnnouncementService.kt create mode 100644 src/main/kotlin/app/revanced/api/services/ApiService.kt rename src/main/kotlin/app/revanced/api/{modules/Security.kt => services/AuthService.kt} (87%) create mode 100644 src/main/kotlin/app/revanced/api/services/PatchesService.kt delete mode 100644 src/test/kotlin/app/revanced/ApplicationTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index c9b481ba..a62319d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,21 +11,11 @@ tasks { expand("projectVersion" to project.version) } - /* - Dummy task to hack gradle-semantic-release-plugin to release this project. - - Explanation: - SemVer is a standard for versioning libraries. - For that reason the semantic-release plugin uses the "publish" task to publish libraries. - However, this subproject is not a library, and the "publish" task is not available for this subproject. - Because semantic-release is not designed to handle this case, we need to hack it. - - RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - */ + // Needed by gradle-semantic-release-plugin. + // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 register("publish") { group = "publishing" - description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" - dependsOn(startShadowScripts) + dependsOn(shadowJar) } } @@ -42,8 +32,15 @@ ktor { repositories { mavenCentral() google() - maven { url = uri("https://jitpack.io") } mavenLocal() + maven { + // A repository must be specified for some reason. "registry" is a dummy. + url = uri("https://maven.pkg.github.com/revanced/registry") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") + } + } } dependencies { @@ -78,8 +75,4 @@ dependencies { implementation(libs.revanced.patcher) implementation(libs.revanced.library) implementation(libs.caffeine) - - testImplementation(libs.mockk) - testImplementation(libs.ktor.server.tests) - testImplementation(libs.kotlin.test.junit) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f00cb0f7..66cbed37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "1.9.22" +kotlin = "2.0.0" logback = "1.4.14" exposed = "0.41.1" h2 = "2.2.224" @@ -7,11 +7,10 @@ koin = "3.5.3" dotenv = "6.4.1" ktor = "2.3.7" ktoml = "0.5.1" -picocli = "4.7.3" +picocli = "4.7.5" datetime = "0.5.0" -mockk = "1.13.9" -revanced-patcher = "19.2.0" -revanced-library = "1.5.0" +revanced-patcher = "19.3.1" +revanced-library = "2.3.0" caffeine = "3.1.8" [libraries] @@ -39,13 +38,10 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" } dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" } -ktor-server-tests = { module = "io.ktor:ktor-server-tests" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } -mockk = { module = "io.mockk:mockk", version.ref = "mockk" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..8a1f6b97 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..b740cf13 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32..25da30db 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index b1898ef9..585730a7 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -1,6 +1,7 @@ package app.revanced.api.command -import app.revanced.api.modules.* +import app.revanced.api.configuration.* +import app.revanced.api.configuration.routing.configureRouting import io.ktor.server.engine.* import io.ktor.server.netty.* import picocli.CommandLine @@ -27,7 +28,7 @@ internal object StartAPICommand : Runnable { override fun run() { embeddedServer(Netty, port, host) { configureDependencies() - configureHTTP() + configureHTTP(allowedHost = host) configureSerialization() configureSecurity() configureRouting() diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt new file mode 100644 index 00000000..dc8f6204 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -0,0 +1,83 @@ +package app.revanced.api.configuration + +import app.revanced.api.repository.AnnouncementRepository +import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.repository.backend.github.GitHubBackendRepository +import app.revanced.api.services.AnnouncementService +import app.revanced.api.services.ApiService +import app.revanced.api.services.AuthService +import app.revanced.api.services.PatchesService +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.source.decodeFromStream +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.server.application.* +import org.jetbrains.exposed.sql.Database +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.ktor.plugin.Koin +import java.io.File + +fun Application.configureDependencies() { + val globalModule = module { + single { + Dotenv.configure() + .systemProperties() + .load() + } + } + + val repositoryModule = module { + single { + val dotenv = get() + + Database.connect( + url = dotenv["DB_URL"], + user = dotenv["DB_USER"], + password = dotenv["DB_PASSWORD"], + driver = "org.h2.Driver", + ) + } + + single { + val configFilePath = get()["CONFIG_FILE_PATH"] + val configFile = File(configFilePath).inputStream() + + Toml.decodeFromStream(configFile) + } + + singleOf(::AnnouncementRepository) + } + + val serviceModule = module { + single { + val dotenv = get() + + val jwtSecret = dotenv["JWT_SECRET"] + val issuer = dotenv["JWT_ISSUER"] + val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt() + + val basicUsername = dotenv["BASIC_USERNAME"] + val basicPassword = dotenv["BASIC_PASSWORD"] + + AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) + } + single { + val token = get()["GITHUB_TOKEN"] + + GitHubBackendRepository(token) + } bind BackendRepository::class + singleOf(::AnnouncementService) + singleOf(::PatchesService) + singleOf(::ApiService) + } + + install(Koin) { + modules( + globalModule, + repositoryModule, + serviceModule, + ) + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt similarity index 81% rename from src/main/kotlin/app/revanced/api/modules/HTTP.kt rename to src/main/kotlin/app/revanced/api/configuration/HTTP.kt index 1f3248f9..336b100b 100644 --- a/src/main/kotlin/app/revanced/api/modules/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -1,4 +1,4 @@ -package app.revanced.api.modules +package app.revanced.api.configuration import io.ktor.http.* import io.ktor.http.content.* @@ -8,7 +8,9 @@ import io.ktor.server.plugins.conditionalheaders.* import io.ktor.server.plugins.cors.routing.* import kotlin.time.Duration.Companion.minutes -fun Application.configureHTTP() { +fun Application.configureHTTP( + allowedHost: String, +) { install(ConditionalHeaders) install(CORS) { allowMethod(HttpMethod.Options) @@ -16,7 +18,7 @@ fun Application.configureHTTP() { allowMethod(HttpMethod.Delete) allowMethod(HttpMethod.Patch) allowHeader(HttpHeaders.Authorization) - anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + allowHost(allowedHost) } install(CachingHeaders) { options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } diff --git a/src/main/kotlin/app/revanced/api/configuration/Security.kt b/src/main/kotlin/app/revanced/api/configuration/Security.kt new file mode 100644 index 00000000..2543fb14 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Security.kt @@ -0,0 +1,9 @@ +package app.revanced.api.configuration + +import app.revanced.api.services.AuthService +import io.ktor.server.application.* +import org.koin.ktor.ext.get + +fun Application.configureSecurity() { + get().configureSecurity(this) +} diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt new file mode 100644 index 00000000..4e9f7ed0 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -0,0 +1,19 @@ +package app.revanced.api.configuration + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy + +@OptIn(ExperimentalSerializationApi::class) +fun Application.configureSerialization() { + install(ContentNegotiation) { + json( + Json { + namingStrategy = JsonNamingStrategy.SnakeCase + }, + ) + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt new file mode 100644 index 00000000..18420b6c --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt @@ -0,0 +1,19 @@ +package app.revanced.api.configuration.routing + +import app.revanced.api.configuration.routing.routes.configureAnnouncementsRoute +import app.revanced.api.configuration.routing.routes.configurePatchesRoute +import app.revanced.api.configuration.routing.routes.configureRootRoute +import app.revanced.api.repository.ConfigurationRepository +import io.ktor.server.application.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get + +internal fun Application.configureRouting() = routing { + val configuration = get() + + route("/v${configuration.apiVersion}") { + configureRootRoute() + configurePatchesRoute() + configureAnnouncementsRoute() + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt new file mode 100644 index 00000000..4f9f038b --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -0,0 +1,86 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.schema.APIAnnouncement +import app.revanced.api.schema.APIAnnouncementArchivedAt +import app.revanced.api.services.AnnouncementService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.util.* +import org.koin.ktor.ext.get as koinGet + +internal fun Route.configureAnnouncementsRoute() = route("/announcements") { + val announcementService = koinGet() + + route("/{channel}/latest") { + get("/id") { + val channel: String by call.parameters + + call.respond( + announcementService.latestId(channel) ?: return@get call.respond(HttpStatusCode.NotFound), + ) + } + + get { + val channel: String by call.parameters + + call.respond( + announcementService.latest(channel) ?: return@get call.respond(HttpStatusCode.NotFound), + ) + } + } + + get("/{channel}") { + val channel: String by call.parameters + + call.respond(announcementService.all(channel)) + } + + route("/latest") { + get("/id") { + call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) + } + + get { + call.respond(announcementService.latest() ?: return@get call.respond(HttpStatusCode.NotFound)) + } + } + + get { + call.respond(announcementService.all()) + } + + authenticate("jwt") { + post { + announcementService.new(call.receive()) + } + + post("/{id}/archive") { + val id: Int by call.parameters + val archivedAt = call.receiveNullable()?.archivedAt + + announcementService.archive(id, archivedAt) + } + + post("/{id}/unarchive") { + val id: Int by call.parameters + + announcementService.unarchive(id) + } + + patch("/{id}") { + val id: Int by call.parameters + + announcementService.update(id, call.receive()) + } + + delete("/{id}") { + val id: Int by call.parameters + + announcementService.delete(id) + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt new file mode 100644 index 00000000..b502ac82 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -0,0 +1,41 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.services.ApiService +import app.revanced.api.services.AuthService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get + +internal fun Route.configureRootRoute() { + val apiService = get() + val authService = get() + + get("/contributors") { + call.respond(apiService.contributors()) + } + + get("/team") { + call.respond(apiService.team()) + } + + route("/ping") { + handle { + call.respond(HttpStatusCode.NoContent) + } + } + + authenticate("basic") { + get("/token") { + call.respond(authService.newToken()) + } + } + + staticResources("/", "/static/api") { + contentType { ContentType.Application.Json } + extensions("json") + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt new file mode 100644 index 00000000..21e811ef --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt @@ -0,0 +1,26 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.services.PatchesService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get as koinGet + +internal fun Route.configurePatchesRoute() = route("/patches") { + val patchesService = koinGet() + + route("latest") { + get { + call.respond(patchesService.latestRelease()) + } + + get("/version") { + call.respond(patchesService.latestVersion()) + } + + get("/list") { + call.respondBytes(ContentType.Application.Json) { patchesService.list() } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt deleted file mode 100644 index cf8afe07..00000000 --- a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.revanced.api.modules - -import app.revanced.api.backend.Backend -import app.revanced.api.backend.github.GitHubBackend -import app.revanced.api.schema.APIConfiguration -import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.source.decodeFromStream -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.server.application.* -import org.jetbrains.exposed.sql.Database -import org.koin.dsl.bind -import org.koin.dsl.module -import org.koin.ktor.plugin.Koin -import java.io.File - -fun Application.configureDependencies() { - install(Koin) { - modules( - globalModule, - gitHubBackendModule, - databaseModule, - authModule, - ) - } -} - -val globalModule = module { - single { - Dotenv.configure() - .systemProperties() - .load() - } - single { - val configFilePath = get()["CONFIG_FILE_PATH"] - Toml.decodeFromStream(File(configFilePath).inputStream()) - } -} - -val gitHubBackendModule = module { - single { - val token = get()["GITHUB_TOKEN"] - GitHubBackend(token) - } bind Backend::class -} - -val databaseModule = module { - single { - val dotenv = get() - - Database.connect( - url = dotenv["DB_URL"], - user = dotenv["DB_USER"], - password = dotenv["DB_PASSWORD"], - driver = "org.h2.Driver", - ) - } - factory { - AnnouncementService(get()) - } -} - -val authModule = module { - single { - val dotenv = get() - - val jwtSecret = dotenv["JWT_SECRET"] - val issuer = dotenv["JWT_ISSUER"] - val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt() - - val basicUsername = dotenv["BASIC_USERNAME"] - val basicPassword = dotenv["BASIC_PASSWORD"] - - AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) - } -} diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt deleted file mode 100644 index 616e2e5e..00000000 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ /dev/null @@ -1,232 +0,0 @@ -package app.revanced.api.modules - -import app.revanced.api.backend.Backend -import app.revanced.api.schema.* -import app.revanced.library.PatchUtils -import app.revanced.patcher.PatchBundleLoader -import com.github.benmanes.caffeine.cache.Caffeine -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.http.content.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.util.pipeline.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import java.io.ByteArrayOutputStream -import java.net.URL -import org.koin.ktor.ext.get as koinGet - -fun Application.configureRouting() { - val backend: Backend = koinGet() - val configuration: APIConfiguration = koinGet() - val announcementService: AnnouncementService = koinGet() - val authService: AuthService = koinGet() - - routing { - route("/v${configuration.apiVersion}") { - route("/announcements") { - suspend fun PipelineContext<*, ApplicationCall>.announcement(block: AnnouncementService.() -> APIResponseAnnouncement?) = - announcementService.block()?.let { call.respond(it) } - ?: call.respond(HttpStatusCode.NotFound) - - suspend fun PipelineContext<*, ApplicationCall>.announcementId(block: AnnouncementService.() -> APILatestAnnouncement?) = - announcementService.block()?.let { call.respond(it) } - ?: call.respond(HttpStatusCode.NotFound) - - suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) = - block(call.parameters["channel"]!!) - - route("/{channel}/latest") { - get("/id") { - channel { - announcementId { - latestId(it) - } - } - } - - get { - channel { - announcement { - latest(it) - } - } - } - } - - get("/{channel}") { - channel { - call.respond(announcementService.read(it)) - } - } - - route("/latest") { - get("/id") { - announcementId { - latestId() - } - } - - get { - announcement { - latest() - } - } - } - - get { - call.respond(announcementService.read()) - } - - authenticate("jwt") { - suspend fun PipelineContext<*, ApplicationCall>.id(block: suspend (Int) -> Unit) = - call.parameters["id"]!!.toIntOrNull()?.let { - block(it) - } ?: call.respond(HttpStatusCode.BadRequest) - - post { - announcementService.new(call.receive()) - } - - post("/{id}/archive") { - id { - val archivedAt = call.receiveNullable()?.archivedAt - announcementService.archive(it, archivedAt) - } - } - - post("/{id}/unarchive") { - id { - announcementService.unarchive(it) - } - } - - patch("/{id}") { - id { - announcementService.update(it, call.receive()) - } - } - - delete("/{id}") { - id { - announcementService.delete(it) - } - } - } - } - - route("/patches") { - route("latest") { - get { - val patchesRelease = - backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrationsReleases = configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) - .map { APIAsset(it.downloadUrl) } - .filter { it.type != APIAsset.Type.UNKNOWN } - .toSet() - - val apiRelease = APIRelease( - patchesRelease.tag, - patchesRelease.createdAt, - patchesRelease.releaseNote, - assets, - ) - - call.respond(apiRelease) - } - - get("/version") { - val patchesRelease = - backend.getRelease(configuration.organization, configuration.patchesRepository) - - val apiPatchesRelease = APIReleaseVersion(patchesRelease.tag) - - call.respond(apiPatchesRelease) - } - - val patchesListCache = Caffeine - .newBuilder() - .maximumSize(1) - .build() - - get("/list") { - val patchesRelease = - backend.getRelease(configuration.organization, configuration.patchesRepository) - - val patchesListByteArray = patchesListCache.getIfPresent(patchesRelease.tag) ?: run { - val downloadUrl = patchesRelease.assets - .map { APIAsset(it.downloadUrl) } - .find { it.type == APIAsset.Type.PATCHES } - ?.downloadUrl - - val patches = kotlin.io.path.createTempFile().toFile().apply { - outputStream().use { URL(downloadUrl).openStream().copyTo(it) } - }.let { file -> - PatchBundleLoader.Jar(file).also { file.delete() } - } - - ByteArrayOutputStream().use { stream -> - PatchUtils.Json.serialize(patches, outputStream = stream) - - stream.toByteArray() - }.also { - patchesListCache.put(patchesRelease.tag, it) - } - } - - call.respondBytes(ContentType.Application.Json) { patchesListByteArray } - } - } - } - - staticResources("/", "/static/api") { - contentType { ContentType.Application.Json } - extensions("json") - } - - get("/contributors") { - val contributors = - configuration.contributorsRepositoryNames.map { - async { - APIContributable( - it, - backend.getContributors(configuration.organization, it).map { - APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet(), - ) - } - }.awaitAll() - - call.respond(contributors) - } - - get("/team") { - val team = - backend.getMembers(configuration.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) - } - - call.respond(team) - } - - route("/ping") { - handle { - call.respond(HttpStatusCode.NoContent) - } - } - - authenticate("basic") { - get("/token") { - call.respond(authService.newToken()) - } - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/modules/Serialization.kt b/src/main/kotlin/app/revanced/api/modules/Serialization.kt deleted file mode 100644 index ef385585..00000000 --- a/src/main/kotlin/app/revanced/api/modules/Serialization.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.api.modules - -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.application.* -import io.ktor.server.plugins.contentnegotiation.* - -fun Application.configureSerialization() { - install(ContentNegotiation) { - json() - } -} diff --git a/src/main/kotlin/app/revanced/api/modules/Database.kt b/src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt similarity index 52% rename from src/main/kotlin/app/revanced/api/modules/Database.kt rename to src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt index 4727a0b4..a6c4b633 100644 --- a/src/main/kotlin/app/revanced/api/modules/Database.kt +++ b/src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt @@ -1,6 +1,6 @@ -package app.revanced.api.modules +package app.revanced.api.repository -import app.revanced.api.modules.AnnouncementService.Attachments.announcement +import app.revanced.api.repository.AnnouncementRepository.AttachmentTable.announcement import app.revanced.api.schema.APIAnnouncement import app.revanced.api.schema.APILatestAnnouncement import app.revanced.api.schema.APIResponseAnnouncement @@ -10,96 +10,54 @@ import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.transactions.transaction -class AnnouncementService(private val database: Database) { - private object Announcements : IntIdTable() { - val author = varchar("author", 32).nullable() - val title = varchar("title", 64) - val content = text("content").nullable() - val channel = varchar("channel", 16).nullable() - val createdAt = datetime("createdAt") - val archivedAt = datetime("archivedAt").nullable() - val level = integer("level") - } - - private object Attachments : IntIdTable() { - val url = varchar("url", 256) - val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) - } - - class Announcement(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(Announcements) - - var author by Announcements.author - var title by Announcements.title - var content by Announcements.content - val attachments by Attachment referrersOn announcement - var channel by Announcements.channel - var createdAt by Announcements.createdAt - var archivedAt by Announcements.archivedAt - var level by Announcements.level - - fun api() = APIResponseAnnouncement( - id.value, - author, - title, - content, - attachments.map(Attachment::url).toSet(), - channel, - createdAt, - archivedAt, - level, - ) - } - - class Attachment(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(Attachments) - - var url by Attachments.url - var announcement by Announcement referencedOn Attachments.announcement - } - +internal class AnnouncementRepository(private val database: Database) { init { transaction { - SchemaUtils.create(Announcements, Attachments) + SchemaUtils.create(AnnouncementTable, AttachmentTable) } } - private fun transaction(block: Transaction.() -> T) = transaction(database, block) - - fun read() = transaction { - Announcement.all().map { it.api() }.toSet() + fun all() = transaction { + buildSet { + AnnouncementEntity.all().forEach { announcement -> + add(announcement.toApi()) + } + } } - fun read(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.map { it.api() }.toSet() + fun all(channel: String) = transaction { + buildSet { + AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement -> + add(announcement.toApi()) + } + } } fun delete(id: Int) = transaction { - val announcement = Announcement.findById(id) ?: return@transaction + val announcement = AnnouncementEntity.findById(id) ?: return@transaction announcement.delete() } fun latest() = transaction { - Announcement.all().maxByOrNull { it.createdAt }?.api() + AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() } fun latest(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.api() + AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi() } fun latestId() = transaction { - Announcement.all().maxByOrNull { it.createdAt }?.id?.value?.let { + AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { APILatestAnnouncement(it) } } fun latestId(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { + AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { APILatestAnnouncement(it) } } @@ -108,19 +66,19 @@ class AnnouncementService(private val database: Database) { id: Int, archivedAt: LocalDateTime?, ) = transaction { - Announcement.findById(id)?.apply { + AnnouncementEntity.findById(id)?.apply { this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() } } fun unarchive(id: Int) = transaction { - Announcement.findById(id)?.apply { + AnnouncementEntity.findById(id)?.apply { archivedAt = null } } fun new(new: APIAnnouncement) = transaction { - Announcement.new announcement@{ + AnnouncementEntity.new announcement@{ author = new.author title = new.title content = new.content @@ -130,7 +88,7 @@ class AnnouncementService(private val database: Database) { level = new.level }.also { newAnnouncement -> new.attachmentUrls.map { - Attachment.new { + AttachmentEntity.new { url = it announcement = newAnnouncement } @@ -139,7 +97,7 @@ class AnnouncementService(private val database: Database) { } fun update(id: Int, new: APIAnnouncement) = transaction { - Announcement.findById(id)?.apply { + AnnouncementEntity.findById(id)?.apply { author = new.author title = new.title content = new.content @@ -147,13 +105,66 @@ class AnnouncementService(private val database: Database) { archivedAt = new.archivedAt level = new.level - attachments.forEach(Attachment::delete) + attachments.forEach(AttachmentEntity::delete) new.attachmentUrls.map { - Attachment.new { + AttachmentEntity.new { url = it announcement = this@apply } } } } + + private fun transaction(block: Transaction.() -> T) = transaction(database, block) + + private object AnnouncementTable : IntIdTable() { + val author = varchar("author", 32).nullable() + val title = varchar("title", 64) + val content = text("content").nullable() + val channel = varchar("channel", 16).nullable() + val createdAt = datetime("createdAt") + val archivedAt = datetime("archivedAt").nullable() + val level = integer("level") + } + + private object AttachmentTable : IntIdTable() { + val url = varchar("url", 256) + val announcement = reference("announcement", AnnouncementTable, onDelete = ReferenceOption.CASCADE) + } + + class AnnouncementEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(AnnouncementTable) + + var author by AnnouncementTable.author + var title by AnnouncementTable.title + var content by AnnouncementTable.content + val attachments by AttachmentEntity referrersOn announcement + var channel by AnnouncementTable.channel + var createdAt by AnnouncementTable.createdAt + var archivedAt by AnnouncementTable.archivedAt + var level by AnnouncementTable.level + + fun toApi() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachmentUrls = buildSet { + attachments.forEach { + add(it.url) + } + }, + channel, + createdAt, + archivedAt, + level, + ) + } + + class AttachmentEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(AttachmentTable) + + var url by AttachmentTable.url + var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement + } } diff --git a/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt b/src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt similarity index 85% rename from src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt rename to src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt index 5eff0b6c..531de56e 100644 --- a/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt +++ b/src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt @@ -1,10 +1,10 @@ -package app.revanced.api.schema +package app.revanced.api.repository import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -class APIConfiguration( +internal class ConfigurationRepository( val organization: String, @SerialName("patches-repository") val patchesRepository: String, diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt similarity index 91% rename from src/main/kotlin/app/revanced/api/backend/Backend.kt rename to src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt index c3e53c1c..47ec9d8a 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt @@ -1,4 +1,4 @@ -package app.revanced.api.backend +package app.revanced.api.repository.backend import io.ktor.client.* import io.ktor.client.engine.okhttp.* @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable * * @param httpClientConfig The configuration of the HTTP client. */ -abstract class Backend( +abstract class BackendRepository internal constructor( httpClientConfig: HttpClientConfig.() -> Unit = {}, ) { protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) @@ -114,7 +114,7 @@ abstract class Backend( * @param tag The tag of the release. If null, the latest release is returned. * @return The release. */ - abstract suspend fun getRelease( + abstract suspend fun release( owner: String, repository: String, tag: String? = null, @@ -127,7 +127,7 @@ abstract class Backend( * @param repository The name of the repository. * @return The contributors. */ - abstract suspend fun getContributors(owner: String, repository: String): Set + abstract suspend fun contributors(owner: String, repository: String): Set /** * Get the members of an organization. @@ -135,5 +135,5 @@ abstract class Backend( * @param organization The name of the organization. * @return The members. */ - abstract suspend fun getMembers(organization: String): Set + abstract suspend fun members(organization: String): Set } diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt similarity index 67% rename from src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt rename to src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt index be0db97b..5687f72d 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt @@ -1,18 +1,18 @@ -package app.revanced.api.backend.github +package app.revanced.api.repository.backend.github -import app.revanced.api.backend.Backend -import app.revanced.api.backend.Backend.BackendOrganization.BackendMember -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset -import app.revanced.api.backend.github.api.Request -import app.revanced.api.backend.github.api.Request.Organization.Members -import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors -import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases -import app.revanced.api.backend.github.api.Response -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendMember +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset +import app.revanced.api.repository.backend.github.api.Request +import app.revanced.api.repository.backend.github.api.Request.Organization.Members +import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Contributors +import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Releases +import app.revanced.api.repository.backend.github.api.Response +import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember +import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease import io.ktor.client.call.* import io.ktor.client.plugins.* import io.ktor.client.plugins.auth.* @@ -30,7 +30,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy @OptIn(ExperimentalSerializationApi::class) -class GitHubBackend(token: String? = null) : Backend({ +class GitHubBackendRepository(token: String? = null) : BackendRepository({ install(HttpCache) install(Resources) install(ContentNegotiation) { @@ -59,7 +59,7 @@ class GitHubBackend(token: String? = null) : Backend({ } } }) { - override suspend fun getRelease( + override suspend fun release( owner: String, repository: String, tag: String?, @@ -80,7 +80,7 @@ class GitHubBackend(token: String? = null) : Backend({ ) } - override suspend fun getContributors( + override suspend fun contributors( owner: String, repository: String, ): Set { @@ -96,7 +96,7 @@ class GitHubBackend(token: String? = null) : Backend({ }.toSet() } - override suspend fun getMembers(organization: String): Set { + override suspend fun members(organization: String): Set { // Get the list of members of the organization. val members: Set = client.get(Members(organization)).body() diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt similarity index 93% rename from src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt rename to src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt index dc480ba6..557f9e23 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt @@ -1,4 +1,4 @@ -package app.revanced.api.backend.github.api +package app.revanced.api.repository.backend.github.api import io.ktor.resources.* diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt similarity index 96% rename from src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt rename to src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt index 693a8975..2ddc8f1f 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt @@ -1,4 +1,4 @@ -package app.revanced.api.backend.github.api +package app.revanced.api.repository.backend.github.api import kotlinx.datetime.Instant import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt new file mode 100644 index 00000000..63418da8 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt @@ -0,0 +1,35 @@ +package app.revanced.api.services + +import app.revanced.api.repository.AnnouncementRepository +import app.revanced.api.schema.APIAnnouncement +import app.revanced.api.schema.APILatestAnnouncement +import kotlinx.datetime.LocalDateTime + +internal class AnnouncementService( + private val announcementRepository: AnnouncementRepository, +) { + fun latestId(channel: String): APILatestAnnouncement? = announcementRepository.latestId(channel) + fun latestId(): APILatestAnnouncement? = announcementRepository.latestId() + + fun latest(channel: String) = announcementRepository.latest(channel) + fun latest() = announcementRepository.latest() + + fun all(channel: String) = announcementRepository.all(channel) + fun all() = announcementRepository.all() + + fun new(new: APIAnnouncement) { + announcementRepository.new(new) + } + fun archive(id: Int, archivedAt: LocalDateTime?) { + announcementRepository.archive(id, archivedAt) + } + fun unarchive(id: Int) { + announcementRepository.unarchive(id) + } + fun update(id: Int, new: APIAnnouncement) { + announcementRepository.update(id, new) + } + fun delete(id: Int) { + announcementRepository.delete(id) + } +} diff --git a/src/main/kotlin/app/revanced/api/services/ApiService.kt b/src/main/kotlin/app/revanced/api/services/ApiService.kt new file mode 100644 index 00000000..d36e64b0 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/services/ApiService.kt @@ -0,0 +1,33 @@ +package app.revanced.api.services + +import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.schema.APIContributable +import app.revanced.api.schema.APIContributor +import app.revanced.api.schema.APIMember +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext + +internal class ApiService( + private val backendRepository: BackendRepository, + private val configurationRepository: ConfigurationRepository, +) { + suspend fun contributors() = withContext(Dispatchers.IO) { + configurationRepository.contributorsRepositoryNames.map { + async { + APIContributable( + it, + backendRepository.contributors(configurationRepository.organization, it).map { + APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + }.toSet(), + ) + } + } + }.awaitAll() + + suspend fun team() = backendRepository.members(configurationRepository.organization).map { + APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Security.kt b/src/main/kotlin/app/revanced/api/services/AuthService.kt similarity index 87% rename from src/main/kotlin/app/revanced/api/modules/Security.kt rename to src/main/kotlin/app/revanced/api/services/AuthService.kt index 02cc5f3c..af824a0b 100644 --- a/src/main/kotlin/app/revanced/api/modules/Security.kt +++ b/src/main/kotlin/app/revanced/api/services/AuthService.kt @@ -1,15 +1,14 @@ -package app.revanced.api.modules +package app.revanced.api.services import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* -import org.koin.ktor.ext.get import java.util.* import kotlin.time.Duration.Companion.minutes -class AuthService( +internal class AuthService( private val issuer: String, private val validityInMin: Int, private val jwtSecret: String, @@ -46,8 +45,3 @@ class AuthService( .sign(Algorithm.HMAC256(jwtSecret)) } } - -fun Application.configureSecurity() { - val configureSecurity = get().configureSecurity - configureSecurity() -} diff --git a/src/main/kotlin/app/revanced/api/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/services/PatchesService.kt new file mode 100644 index 00000000..22afd709 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/services/PatchesService.kt @@ -0,0 +1,87 @@ +package app.revanced.api.services + +import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.schema.APIAsset +import app.revanced.api.schema.APIRelease +import app.revanced.api.schema.APIReleaseVersion +import app.revanced.library.PatchUtils +import app.revanced.patcher.PatchBundleLoader +import com.github.benmanes.caffeine.cache.Caffeine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.net.URL + +internal class PatchesService( + private val backendRepository: BackendRepository, + private val configurationRepository: ConfigurationRepository, +) { + private val patchesListCache = Caffeine + .newBuilder() + .maximumSize(1) + .build() + + suspend fun latestRelease(): APIRelease { + val patchesRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.patchesRepository, + ) + val integrationsReleases = withContext(Dispatchers.Default) { + configurationRepository.integrationsRepositoryNames.map { + async { backendRepository.release(configurationRepository.organization, it) } + } + }.awaitAll() + + val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) + .map { APIAsset(it.downloadUrl) } + .filter { it.type != APIAsset.Type.UNKNOWN } + .toSet() + + return APIRelease( + patchesRelease.tag, + patchesRelease.createdAt, + patchesRelease.releaseNote, + assets, + ) + } + + suspend fun latestVersion(): APIReleaseVersion { + val patchesRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.patchesRepository, + ) + + return APIReleaseVersion(patchesRelease.tag) + } + + suspend fun list(): ByteArray { + val patchesRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.patchesRepository, + ) + + return patchesListCache.getIfPresent(patchesRelease.tag) ?: run { + val downloadUrl = patchesRelease.assets + .map { APIAsset(it.downloadUrl) } + .find { it.type == APIAsset.Type.PATCHES } + ?.downloadUrl + + val patches = kotlin.io.path.createTempFile().toFile().apply { + outputStream().use { URL(downloadUrl).openStream().copyTo(it) } + }.let { file -> + PatchBundleLoader.Jar(file).also { file.delete() } + } + + ByteArrayOutputStream().use { stream -> + PatchUtils.Json.serialize(patches, outputStream = stream) + + stream.toByteArray() + }.also { + patchesListCache.put(patchesRelease.tag, it) + } + } + } +} diff --git a/src/main/resources/static/api/about.json b/src/main/resources/static/api/about.json index 1a0d1cbf..a947c48a 100644 --- a/src/main/resources/static/api/about.json +++ b/src/main/resources/static/api/about.json @@ -80,4 +80,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt deleted file mode 100644 index 5f1ce2e8..00000000 --- a/src/test/kotlin/app/revanced/ApplicationTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package app.revanced - -import app.revanced.api.modules.* -import app.revanced.api.schema.APIConfiguration -import com.akuleshov7.ktoml.Toml -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import io.ktor.util.* -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.encodeToString -import kotlin.test.* - -class ApplicationTest { - @Test - fun `successfully create a token`() = testApplication { - val apiConfigurationFile = kotlin.io.path.createTempFile().toFile().apply { - Toml.encodeToString( - APIConfiguration( - organization = "ReVanced", - patchesRepository = "", - integrationsRepositoryNames = setOf(), - contributorsRepositoryNames = setOf(), - ), - ).let(::writeText) - - deleteOnExit() - } - - val dotenv = mockk() - every { dotenv[any()] } returns "ReVanced" - every { dotenv["JWT_VALIDITY_IN_MIN"] } returns "5" - every { dotenv["CONFIG_FILE_PATH"] } returns apiConfigurationFile.absolutePath - - application { - configureDependencies() - configureHTTP() - configureSerialization() - configureSecurity() - configureRouting() - } - - val token = client.get("/v1/token") { - headers { - append( - HttpHeaders.Authorization, - "Basic ${"${dotenv["BASIC_USERNAME"]}:${dotenv["BASIC_PASSWORD"]}".encodeBase64()}", - ) - } - }.bodyAsText() - - assert(token.isNotEmpty()) - } -} From 39f54bbb32512a6df255bbec6821f7f7acf91340 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 04:28:36 +0200 Subject: [PATCH 29/81] feat: Add proxy for old API --- .env.example | 2 + .../revanced/api/command/StartAPICommand.kt | 3 + .../api/configuration/Dependencies.kt | 74 +++++++++++++++++-- .../api/configuration/routing/Routing.kt | 16 ++-- .../routing/routes/Announcements.kt | 20 ++--- .../configuration/routing/routes/ApiRoute.kt | 12 +-- .../configuration/routing/routes/OldApi.kt | 16 ++++ .../routing/routes/PatchesRoute.kt | 6 +- .../revanced/api/repository/OldApiService.kt | 65 ++++++++++++++++ .../repository/backend/BackendRepository.kt | 7 +- .../backend/github/GitHubBackendRepository.kt | 42 +---------- .../revanced/api/static}/about.json | 0 .../{ => app/revanced/api}/static/robots.txt | 0 13 files changed, 188 insertions(+), 75 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt create mode 100644 src/main/kotlin/app/revanced/api/repository/OldApiService.kt rename src/main/resources/{static/api => app/revanced/api/static}/about.json (100%) rename src/main/resources/{ => app/revanced/api}/static/robots.txt (100%) diff --git a/.env.example b/.env.example index e3a8ba04..78477d23 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,5 @@ JWT_VALIDITY_IN_MIN= BASIC_USERNAME= BASIC_PASSWORD= + +OLD_API_URL= \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 585730a7..ba473448 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -2,8 +2,11 @@ package app.revanced.api.command import app.revanced.api.configuration.* import app.revanced.api.configuration.routing.configureRouting +import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import picocli.CommandLine @CommandLine.Command( diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index dc8f6204..8a603242 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -2,6 +2,7 @@ package app.revanced.api.configuration import app.revanced.api.repository.AnnouncementRepository import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.OldApiService import app.revanced.api.repository.backend.BackendRepository import app.revanced.api.repository.backend.github.GitHubBackendRepository import app.revanced.api.services.AnnouncementService @@ -11,14 +12,27 @@ import app.revanced.api.services.PatchesService import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.source.decodeFromStream import io.github.cdimascio.dotenv.Dotenv +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.resources.* +import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy import org.jetbrains.exposed.sql.Database import org.koin.core.module.dsl.singleOf -import org.koin.dsl.bind +import org.koin.core.parameter.parameterArrayOf import org.koin.dsl.module import org.koin.ktor.plugin.Koin import java.io.File +@OptIn(ExperimentalSerializationApi::class) fun Application.configureDependencies() { val globalModule = module { single { @@ -26,6 +40,16 @@ fun Application.configureDependencies() { .systemProperties() .load() } + factory { params -> + val defaultRequestUri: String = params.get() + val configBlock = params.getOrNull<(HttpClientConfig.() -> Unit)>() ?: {} + + HttpClient(OkHttp) { + defaultRequest { url(defaultRequestUri) } + + configBlock() + } + } } val repositoryModule = module { @@ -40,6 +64,43 @@ fun Application.configureDependencies() { ) } + single { + GitHubBackendRepository( + get { + val defaultRequestUri = "https://api.github.com" + val configBlock: HttpClientConfig.() -> Unit = { + install(HttpCache) + install(Resources) + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase + }, + ) + } + + get()["GITHUB_TOKEN"]?.let { + install(Auth) { + bearer { + loadTokens { + BearerTokens( + accessToken = it, + refreshToken = "", // Required dummy value + ) + } + + sendWithoutRequest { true } + } + } + } + } + + parameterArrayOf(defaultRequestUri, configBlock) + }, + ) + } + single { val configFilePath = get()["CONFIG_FILE_PATH"] val configFile = File(configFilePath).inputStream() @@ -64,10 +125,13 @@ fun Application.configureDependencies() { AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) } single { - val token = get()["GITHUB_TOKEN"] - - GitHubBackendRepository(token) - } bind BackendRepository::class + OldApiService( + get { + val defaultRequestUri = get()["OLD_API_URL"] + parameterArrayOf(defaultRequestUri) + }, + ) + } singleOf(::AnnouncementService) singleOf(::PatchesService) singleOf(::ApiService) diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt index 18420b6c..8296ee3a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt @@ -1,8 +1,9 @@ package app.revanced.api.configuration.routing -import app.revanced.api.configuration.routing.routes.configureAnnouncementsRoute -import app.revanced.api.configuration.routing.routes.configurePatchesRoute -import app.revanced.api.configuration.routing.routes.configureRootRoute +import app.revanced.api.configuration.routing.routes.announcementsRoute +import app.revanced.api.configuration.routing.routes.oldApiRoute +import app.revanced.api.configuration.routing.routes.patchesRoute +import app.revanced.api.configuration.routing.routes.rootRoute import app.revanced.api.repository.ConfigurationRepository import io.ktor.server.application.* import io.ktor.server.routing.* @@ -12,8 +13,11 @@ internal fun Application.configureRouting() = routing { val configuration = get() route("/v${configuration.apiVersion}") { - configureRootRoute() - configurePatchesRoute() - configureAnnouncementsRoute() + rootRoute() + patchesRoute() + announcementsRoute() } + + // TODO: Remove, once migration period from v2 API is over (In 1-2 years). + oldApiRoute() } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt index 4f9f038b..bc4859d9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -12,11 +12,11 @@ import io.ktor.server.routing.* import io.ktor.server.util.* import org.koin.ktor.ext.get as koinGet -internal fun Route.configureAnnouncementsRoute() = route("/announcements") { +internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() - route("/{channel}/latest") { - get("/id") { + route("{channel}/latest") { + get("id") { val channel: String by call.parameters call.respond( @@ -33,14 +33,14 @@ internal fun Route.configureAnnouncementsRoute() = route("/announcements") { } } - get("/{channel}") { + get("{channel}") { val channel: String by call.parameters call.respond(announcementService.all(channel)) } - route("/latest") { - get("/id") { + route("latest") { + get("id") { call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) } @@ -58,26 +58,26 @@ internal fun Route.configureAnnouncementsRoute() = route("/announcements") { announcementService.new(call.receive()) } - post("/{id}/archive") { + post("{id}/archive") { val id: Int by call.parameters val archivedAt = call.receiveNullable()?.archivedAt announcementService.archive(id, archivedAt) } - post("/{id}/unarchive") { + post("{id}/unarchive") { val id: Int by call.parameters announcementService.unarchive(id) } - patch("/{id}") { + patch("{id}") { val id: Int by call.parameters announcementService.update(id, call.receive()) } - delete("/{id}") { + delete("{id}") { val id: Int by call.parameters announcementService.delete(id) diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt index b502ac82..56eb5531 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -10,31 +10,31 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get -internal fun Route.configureRootRoute() { +internal fun Route.rootRoute() { val apiService = get() val authService = get() - get("/contributors") { + get("contributors") { call.respond(apiService.contributors()) } - get("/team") { + get("team") { call.respond(apiService.team()) } - route("/ping") { + route("ping") { handle { call.respond(HttpStatusCode.NoContent) } } authenticate("basic") { - get("/token") { + get("token") { call.respond(authService.newToken()) } } - staticResources("/", "/static/api") { + staticResources("/", "/app/revanced/api/static") { contentType { ContentType.Application.Json } extensions("json") } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt new file mode 100644 index 00000000..64f8006c --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt @@ -0,0 +1,16 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.repository.OldApiService +import io.ktor.server.application.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get + +internal fun Route.oldApiRoute() { + val oldApiService = get() + + route(Regex("(v2|tools|contributor).*")) { + handle { + oldApiService.proxy(call) + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt index 21e811ef..4ddfd655 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt @@ -7,7 +7,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get as koinGet -internal fun Route.configurePatchesRoute() = route("/patches") { +internal fun Route.patchesRoute() = route("patches") { val patchesService = koinGet() route("latest") { @@ -15,11 +15,11 @@ internal fun Route.configurePatchesRoute() = route("/patches") { call.respond(patchesService.latestRelease()) } - get("/version") { + get("version") { call.respond(patchesService.latestVersion()) } - get("/list") { + get("list") { call.respondBytes(ContentType.Application.Json) { patchesService.list() } } } diff --git a/src/main/kotlin/app/revanced/api/repository/OldApiService.kt b/src/main/kotlin/app/revanced/api/repository/OldApiService.kt new file mode 100644 index 00000000..4c5612da --- /dev/null +++ b/src/main/kotlin/app/revanced/api/repository/OldApiService.kt @@ -0,0 +1,65 @@ +package app.revanced.api.repository + +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.util.* +import io.ktor.utils.io.* + +internal class OldApiService(private val client: HttpClient) { + @OptIn(InternalAPI::class) + suspend fun proxy(call: ApplicationCall) { + val channel = call.request.receiveChannel() + val size = channel.availableForRead + val byteArray = ByteArray(size) + channel.readFully(byteArray) + + val response: HttpResponse = client.request(call.request.uri) { + method = call.request.httpMethod + + headers { + appendAll( + call.request.headers.filter { key, _ -> + !key.equals( + HttpHeaders.ContentType, + ignoreCase = true, + ) && !key.equals( + HttpHeaders.ContentLength, + ignoreCase = true, + ) && !key.equals(HttpHeaders.Host, ignoreCase = true) + }, + ) + } + if (call.request.httpMethod == HttpMethod.Post) { + body = ByteArrayContent(byteArray, call.request.contentType()) + } + } + + val headers = response.headers + + call.respond(object : OutgoingContent.WriteChannelContent() { + override val contentLength: Long? = headers[HttpHeaders.ContentLength]?.toLong() + override val contentType = headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) } + override val headers: Headers = Headers.build { + appendAll( + headers.filter { key, _ -> + !key.equals( + HttpHeaders.ContentType, + ignoreCase = true, + ) && !key.equals(HttpHeaders.ContentLength, ignoreCase = true) + }, + ) + } + override val status = response.status + + override suspend fun writeTo(channel: ByteWriteChannel) { + response.content.copyAndClose(channel) + } + }) + } +} diff --git a/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt index 47ec9d8a..f27012d9 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt @@ -1,20 +1,17 @@ package app.revanced.api.repository.backend import io.ktor.client.* -import io.ktor.client.engine.okhttp.* import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable /** * The backend of the application used to get data for the API. * - * @param httpClientConfig The configuration of the HTTP client. + * @param client The HTTP client to use for requests. */ abstract class BackendRepository internal constructor( - httpClientConfig: HttpClientConfig.() -> Unit = {}, + protected val client: HttpClient, ) { - protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) - /** * A user. * diff --git a/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt index 5687f72d..1664adf3 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt @@ -13,52 +13,14 @@ import app.revanced.api.repository.backend.github.api.Response import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease +import io.ktor.client.* import io.ktor.client.call.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* -import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.resources.* -import io.ktor.client.plugins.resources.Resources -import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNamingStrategy -@OptIn(ExperimentalSerializationApi::class) -class GitHubBackendRepository(token: String? = null) : BackendRepository({ - install(HttpCache) - install(Resources) - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - namingStrategy = JsonNamingStrategy.SnakeCase - }, - ) - } - - defaultRequest { url("https://api.github.com") } - - token?.let { - install(Auth) { - bearer { - loadTokens { - BearerTokens( - accessToken = it, - refreshToken = "", // Required dummy value - ) - } - - sendWithoutRequest { true } - } - } - } -}) { +class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { override suspend fun release( owner: String, repository: String, diff --git a/src/main/resources/static/api/about.json b/src/main/resources/app/revanced/api/static/about.json similarity index 100% rename from src/main/resources/static/api/about.json rename to src/main/resources/app/revanced/api/static/about.json diff --git a/src/main/resources/static/robots.txt b/src/main/resources/app/revanced/api/static/robots.txt similarity index 100% rename from src/main/resources/static/robots.txt rename to src/main/resources/app/revanced/api/static/robots.txt From 09043904818aa37e151cce4a64b17805933ad04c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 04:32:27 +0200 Subject: [PATCH 30/81] refactor: Lint & code cleanup --- .../app/revanced/api/command/StartAPICommand.kt | 8 ++++---- .../app/revanced/api/repository/OldApiService.kt | 13 ++++++++----- .../kotlin/app/revanced/api/services/AuthService.kt | 10 ++++------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index ba473448..88e69455 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -1,12 +1,12 @@ package app.revanced.api.command -import app.revanced.api.configuration.* +import app.revanced.api.configuration.configureDependencies +import app.revanced.api.configuration.configureHTTP +import app.revanced.api.configuration.configureSecurity +import app.revanced.api.configuration.configureSerialization import app.revanced.api.configuration.routing.configureRouting -import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* -import io.ktor.server.response.* -import io.ktor.server.routing.* import picocli.CommandLine @CommandLine.Command( diff --git a/src/main/kotlin/app/revanced/api/repository/OldApiService.kt b/src/main/kotlin/app/revanced/api/repository/OldApiService.kt index 4c5612da..97a4443e 100644 --- a/src/main/kotlin/app/revanced/api/repository/OldApiService.kt +++ b/src/main/kotlin/app/revanced/api/repository/OldApiService.kt @@ -28,10 +28,12 @@ internal class OldApiService(private val client: HttpClient) { !key.equals( HttpHeaders.ContentType, ignoreCase = true, - ) && !key.equals( - HttpHeaders.ContentLength, - ignoreCase = true, - ) && !key.equals(HttpHeaders.Host, ignoreCase = true) + ) && + !key.equals( + HttpHeaders.ContentLength, + ignoreCase = true, + ) && + !key.equals(HttpHeaders.Host, ignoreCase = true) }, ) } @@ -51,7 +53,8 @@ internal class OldApiService(private val client: HttpClient) { !key.equals( HttpHeaders.ContentType, ignoreCase = true, - ) && !key.equals(HttpHeaders.ContentLength, ignoreCase = true) + ) && + !key.equals(HttpHeaders.ContentLength, ignoreCase = true) }, ) } diff --git a/src/main/kotlin/app/revanced/api/services/AuthService.kt b/src/main/kotlin/app/revanced/api/services/AuthService.kt index af824a0b..29f9a818 100644 --- a/src/main/kotlin/app/revanced/api/services/AuthService.kt +++ b/src/main/kotlin/app/revanced/api/services/AuthService.kt @@ -38,10 +38,8 @@ internal class AuthService( } } - fun newToken(): String { - return JWT.create() - .withIssuer(issuer) - .withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds)) - .sign(Algorithm.HMAC256(jwtSecret)) - } + fun newToken(): String = JWT.create() + .withIssuer(issuer) + .withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds)) + .sign(Algorithm.HMAC256(jwtSecret)) } From 9d816223fb1ac8cded324aa486d9e79a7881ad28 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 14:47:55 +0200 Subject: [PATCH 31/81] build: Update dockerfile and env vars --- .env.example | 17 +++++--- .gitignore | 3 +- Dockerfile | 40 +++++++------------ docker-compose.yml | 20 +++------- .../revanced/api/command/StartAPICommand.kt | 2 +- .../api/configuration/Dependencies.kt | 6 +-- 6 files changed, 34 insertions(+), 54 deletions(-) diff --git a/.env.example b/.env.example index 78477d23..9d7c0e1f 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,20 @@ +# File path to store configurations for the API CONFIG_FILE_PATH=configuration.toml -GITHUB_TOKEN= +# Optional token for API calls to the backend +BACKEND_API_TOKEN= +# An option URL to the old API to proxy for migration purposes +OLD_API_URL= +# Database connection details DB_URL=jdbc:h2:./persistence/revanced-api DB_USER= DB_PASSWORD= -JWT_SECRET= -JWT_ISSUER= -JWT_VALIDITY_IN_MIN= - +# Basic authentication to issue JWT tokens BASIC_USERNAME= BASIC_PASSWORD= -OLD_API_URL= \ No newline at end of file +# JWT configuration for authenticated API endpoints +JWT_SECRET= +JWT_ISSUER= +JWT_VALIDITY_IN_MIN= diff --git a/.gitignore b/.gitignore index a300aea9..8f403a3f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,4 @@ out/ ### Project ### .env persistence/ -configuration.toml -.db \ No newline at end of file +configuration.toml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5a0c2a19..1dcd313a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,19 @@ -FROM azul/zulu-openjdk:latest +# Build the application +FROM gradle:latest AS build -ARG CONFIG_FILE_PATH +ARG GITHUB_ACTOR +ARG GITHUB_TOKEN -ARG DB_URL -ARG DB_USER -ARG DB_PASSWORD +ENV GITHUB_ACTOR $GITHUB_ACTOR +ENV GITHUB_TOKEN $GITHUB_TOKEN -ARG JWT_SECRET -ARG JWT_ISSUER -ARG JWT_VALIDITY_IN_MIN +WORKDIR /app +COPY . . +RUN gradle publish --no-daemon -ARG BASIC_USERNAME -ARG BASIC_PASSWORD +# Build the runtime container +FROM eclipse-temurin:latest -ENV CONFIG_FILE_PATH $CONFIG_FILE_PATH - -ENV DB_URL $DB_URL -ENV DB_USER $DB_USER -ENV DB_PASSWORD $DB_PASSWORD - -ENV JWT_SECRET $JWT_SECRET -ENV JWT_ISSUER $JWT_ISSUER -ENV JWT_VALIDITY_IN_MIN $JWT_VALIDITY_IN_MIN - -ENV BASIC_USERNAME $BASIC_USERNAME -ENV BASIC_PASSWORD $BASIC_PASSWORD - -COPY build/libs/revanced-api-*.jar revanced-api.jar - -CMD java -jar revanced-api.jar start \ No newline at end of file +WORKDIR /app +COPY --from=build /app/build/libs/revanced-api-*.jar revanced-api.jar +CMD java -jar revanced-api.jar $COMMAND diff --git a/docker-compose.yml b/docker-compose.yml index 78d2377c..95b91588 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,13 @@ -version: "3.8" - services: revanced-api: container_name: revanced-api image: ghcr.io/revanced/revanced-api:latest volumes: - - /data/revanced-api/persistence:/persistence - - /data/revanced-api/configuration.toml:/configuration.toml + - /data/revanced-api/persistence:/app/persistence + - /data/revanced-api/.env:/app/.env + - /data/revanced-api/configuration.toml:/app/configuration.toml environment: - - CONFIG_FILE_PATH=configuration.toml - - GITHUB_TOKEN= - - DB_URL=jdbc:h2:./persistence/revanced-api - - DB_USER= - - DB_PASSWORD= - - JWT_SECRET= - - JWT_ISSUER= - - JWT_VALIDITY_IN_MIN=5 - - BASIC_USERNAME= - - BASIC_PASSWORD= + - COMMAND=start ports: - - 127.0.0.1:8080:8000 + - localhost:8080:8080 restart: unless-stopped diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 88e69455..2e78e652 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -19,7 +19,7 @@ internal object StartAPICommand : Runnable { description = ["The host address to bind to."], showDefaultValue = CommandLine.Help.Visibility.ALWAYS, ) - private var host: String = "0.0.0.0" + private var host: String = "127.0.0.1" @CommandLine.Option( names = ["-p", "--port"], diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 8a603242..b4f6542e 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -36,9 +36,7 @@ import java.io.File fun Application.configureDependencies() { val globalModule = module { single { - Dotenv.configure() - .systemProperties() - .load() + Dotenv.configure().load() } factory { params -> val defaultRequestUri: String = params.get() @@ -80,7 +78,7 @@ fun Application.configureDependencies() { ) } - get()["GITHUB_TOKEN"]?.let { + get()["BACKEND_API_TOKEN"]?.let { install(Auth) { bearer { loadTokens { From af47edeb5b152caa4cae3b6334d06f3ef03c27d4 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 14:58:48 +0200 Subject: [PATCH 32/81] docs: Add issue templates and update README --- .github/ISSUE_TEMPLATE/bug_report.yml | 109 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 105 ++++++++++++++++++++ .github/config.yml | 2 + .github/workflows/pull_request.yml | 2 +- README.md | 7 +- configuration.example.toml | 2 +- 7 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..9cae518e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,109 @@ +name: 🐞 Bug report +description: Report a bug or an issue. +title: 'bug: ' +labels: ['Bug report'] +body: + - type: markdown + attributes: + value: | +

+ + + + +
+
+ + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ + # ReVanced API bug report + + Before creating a new bug report, please keep the following in mind: + + - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-api/issues?q=label%3A%22Bug+report%22). + - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-api/blob/main/CONTRIBUTING.md). + - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). + - type: textarea + attributes: + label: Bug description + description: | + - Describe your bug in detail + - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) + - Add images and videos if possible + validations: + required: true + - type: textarea + attributes: + label: Error logs + description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell. + render: shell + - type: textarea + attributes: + label: Solution + description: If applicable, add a possible solution to the bug. + - type: textarea + attributes: + label: Additional context + description: Add additional context here. + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your bug report will be closed if you don't follow the checklist below. + options: + - label: I have checked all open and closed bug reports and this is not a duplicate. + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8bc7f7ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 🗨 Discussions + url: https://github.com/revanced/revanced-suggestions/discussions + about: Have something unspecific to ReVanced APi in mind? Search for or start a new discussion! diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..fab7f3fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,105 @@ +name: ⭐ Feature request +description: Create a detailed request for a new feature. +title: 'feat: ' +labels: ['Feature request'] +body: + - type: markdown + attributes: + value: | +

+ + + + +
+ + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ + # ReVanced APi feature request + + Before creating a new feature request, please keep the following in mind: + + - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-api/issues?q=label%3A%22Feature+request%22). + - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-api/blob/main/CONTRIBUTING.md). + - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). + - type: textarea + attributes: + label: Feature description + description: | + - Describe your feature in detail + - Add images, videos, links, examples, references, etc. if possible + - type: textarea + attributes: + label: Motivation + description: | + A strong motivation is necessary for a feature request to be considered. + + - Why should this feature be implemented? + - What is the explicit use case? + - What are the benefits? + - What makes this feature important? + validations: + required: true + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your feature request will be closed if you don't follow the checklist below. + options: + - label: I have checked all open and closed feature requests and this is not a duplicate + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..075f56b5 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,2 @@ +firstPRMergeComment: > + Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution. diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 41e45541..75b8e67f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,4 +23,4 @@ jobs: destination_branch: 'main' pr_title: 'chore: ${{ env.MESSAGE }}' pr_body: 'This pull request will ${{ env.MESSAGE }}.' - pr_draft: true \ No newline at end of file + pr_draft: true diff --git a/README.md b/README.md index 91ba5df4..8b7732a5 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,11 @@ ReVanced API acts as the data source for [ReVanced Website](https://github.com/R Some of the features ReVanced API include: -- 📢 **Announcements**: Push announcements grouped by channels -- ℹ️ **About**: Get more information such as a description about, ways to donate to, and links of the hoster of ReVanced API -- 🧩 **Patches**: Get the latest updates of ReVanced Patches directly from ReVanced API +- 📢 **Announcements**: Post and get announcements grouped by channels +- ℹ️ **About**: Get more information such as a description, ways to donate to, and links of the hoster of ReVanced API +- 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API - 👥 **Contributors**: List all contributors involved in the project +- 🔄 **Proxy**: A proxy to an old API for migration purposes and backwards compatibility ## 🚀 How to get started diff --git a/configuration.example.toml b/configuration.example.toml index 0fdadb02..5935fd61 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -2,4 +2,4 @@ organization = "org" patches-repository = "patches" integrations-repositories = ["integrations"] contributors-repositories = ["patches", "integrations"] -api-version = 1 \ No newline at end of file +api-version = 1 From 8d5b09bd02809578ee5fb1d519d4b1972e6eb126 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 15:18:27 +0200 Subject: [PATCH 33/81] ci: Setup workflows for proper release --- .github/workflows/build_pull_request.yml | 25 +++++++++++ ...pull_request.yml => open_pull_request.yml} | 0 .github/workflows/release.yml | 42 ++++++++----------- build.gradle.kts | 2 +- 4 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/build_pull_request.yml rename .github/workflows/{pull_request.yml => open_pull_request.yml} (100%) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml new file mode 100644 index 00000000..250871bc --- /dev/null +++ b/.github/workflows/build_pull_request.yml @@ -0,0 +1,25 @@ +name: Build pull request + +on: + workflow_dispatch: + pull_request: + branches: + - dev + +jobs: + release: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache Gradle + uses: burrunan/gradle-cache-action@v1 + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew build --no-daemon diff --git a/.github/workflows/pull_request.yml b/.github/workflows/open_pull_request.yml similarity index 100% rename from .github/workflows/pull_request.yml rename to .github/workflows/open_pull_request.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80433630..b0074423 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,10 +6,6 @@ on: branches: - main - dev - pull_request: - branches: - - main - - dev jobs: release: @@ -44,6 +40,13 @@ jobs: - name: Install dependencies run: npm install + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + fingerprint: ${{ env.GPG_FINGERPRINT }} + - name: Release env: GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} @@ -51,39 +54,30 @@ jobs: - name: Setup QEMU uses: docker/setup-qemu-action@v3 - with: - image: tonistiigi/binfmt:latest - platforms: all - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GH_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker + - name: Extract metadata for the Docker image id: metadata uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ghcr.io/${{ env.IMAGE_NAME }} flavor: | latest=${{ startsWith(github.ref, 'refs/heads/main') }} suffix=-${{ github.sha }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Docker image id: build uses: docker/build-push-action@v5 with: - build-args: GH_TOKEN=${{ secrets.GH_TOKEN }} - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64/v8 - cache-to: type=gha,mode=max,ignore-error=true + platforms: linux/amd64,linux/arm64 cache-from: type=gha + cache-to: type=gha,mode=max,ignore-error=true push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/build.gradle.kts b/build.gradle.kts index a62319d8..bc19853f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ tasks { // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 register("publish") { group = "publishing" - dependsOn(shadowJar) + dependsOn(startShadowScripts) } } From 2430be75d8b24f12eacc8f3a35c2c3d7b6e0dff3 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 17:22:46 +0200 Subject: [PATCH 34/81] chore: Use AGPL license --- LICENSE | 149 ++++++++++++++++++++++++++------------------------------ 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/LICENSE b/LICENSE index f288702d..0ad25db4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. From 659cce3e0300586f31522d663bf2db35bc1c4a6e Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 17:27:25 +0200 Subject: [PATCH 35/81] chore: Move files to correct folders --- .../api/configuration/Dependencies.kt | 18 ++++++------ .../revanced/api/configuration/Security.kt | 2 +- .../repository/AnnouncementRepository.kt | 10 +++---- .../repository/ConfigurationRepository.kt | 2 +- .../repository/backend/BackendRepository.kt | 2 +- .../backend/github/GitHubBackendRepository.kt | 28 +++++++++---------- .../repository/backend/github/api/Request.kt | 2 +- .../repository/backend/github/api/Response.kt | 2 +- .../api/configuration/routing/Routing.kt | 2 +- .../routing/routes/Announcements.kt | 6 ++-- .../configuration/routing/routes/ApiRoute.kt | 4 +-- .../configuration/routing/routes/OldApi.kt | 2 +- .../routing/routes/PatchesRoute.kt | 2 +- .../{ => configuration}/schema/APISchema.kt | 2 +- .../services/AnnouncementService.kt | 8 +++--- .../services/ApiService.kt | 12 ++++---- .../services/AuthService.kt | 2 +- .../services}/OldApiService.kt | 2 +- .../services/PatchesService.kt | 12 ++++---- 19 files changed, 60 insertions(+), 60 deletions(-) rename src/main/kotlin/app/revanced/api/{ => configuration}/repository/AnnouncementRepository.kt (93%) rename src/main/kotlin/app/revanced/api/{ => configuration}/repository/ConfigurationRepository.kt (90%) rename src/main/kotlin/app/revanced/api/{ => configuration}/repository/backend/BackendRepository.kt (98%) rename src/main/kotlin/app/revanced/api/{ => configuration}/repository/backend/github/GitHubBackendRepository.kt (60%) rename src/main/kotlin/app/revanced/api/{ => configuration}/repository/backend/github/api/Request.kt (92%) rename src/main/kotlin/app/revanced/api/{ => configuration}/repository/backend/github/api/Response.kt (95%) rename src/main/kotlin/app/revanced/api/{ => configuration}/schema/APISchema.kt (97%) rename src/main/kotlin/app/revanced/api/{ => configuration}/services/AnnouncementService.kt (80%) rename src/main/kotlin/app/revanced/api/{ => configuration}/services/ApiService.kt (71%) rename src/main/kotlin/app/revanced/api/{ => configuration}/services/AuthService.kt (96%) rename src/main/kotlin/app/revanced/api/{repository => configuration/services}/OldApiService.kt (98%) rename src/main/kotlin/app/revanced/api/{ => configuration}/services/PatchesService.kt (88%) diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index b4f6542e..7cc14f4c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -1,14 +1,14 @@ package app.revanced.api.configuration -import app.revanced.api.repository.AnnouncementRepository -import app.revanced.api.repository.ConfigurationRepository -import app.revanced.api.repository.OldApiService -import app.revanced.api.repository.backend.BackendRepository -import app.revanced.api.repository.backend.github.GitHubBackendRepository -import app.revanced.api.services.AnnouncementService -import app.revanced.api.services.ApiService -import app.revanced.api.services.AuthService -import app.revanced.api.services.PatchesService +import app.revanced.api.configuration.repository.AnnouncementRepository +import app.revanced.api.configuration.repository.ConfigurationRepository +import app.revanced.api.configuration.repository.backend.BackendRepository +import app.revanced.api.configuration.repository.backend.github.GitHubBackendRepository +import app.revanced.api.configuration.services.AnnouncementService +import app.revanced.api.configuration.services.ApiService +import app.revanced.api.configuration.services.AuthService +import app.revanced.api.configuration.services.OldApiService +import app.revanced.api.configuration.services.PatchesService import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.source.decodeFromStream import io.github.cdimascio.dotenv.Dotenv diff --git a/src/main/kotlin/app/revanced/api/configuration/Security.kt b/src/main/kotlin/app/revanced/api/configuration/Security.kt index 2543fb14..1d6d3864 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Security.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Security.kt @@ -1,6 +1,6 @@ package app.revanced.api.configuration -import app.revanced.api.services.AuthService +import app.revanced.api.configuration.services.AuthService import io.ktor.server.application.* import org.koin.ktor.ext.get diff --git a/src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt similarity index 93% rename from src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index a6c4b633..7da8300e 100644 --- a/src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,9 +1,9 @@ -package app.revanced.api.repository +package app.revanced.api.configuration.repository -import app.revanced.api.repository.AnnouncementRepository.AttachmentTable.announcement -import app.revanced.api.schema.APIAnnouncement -import app.revanced.api.schema.APILatestAnnouncement -import app.revanced.api.schema.APIResponseAnnouncement +import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement +import app.revanced.api.configuration.schema.APIAnnouncement +import app.revanced.api.configuration.schema.APILatestAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncement import kotlinx.datetime.* import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass diff --git a/src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt similarity index 90% rename from src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 531de56e..3d5f28c1 100644 --- a/src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -1,4 +1,4 @@ -package app.revanced.api.repository +package app.revanced.api.configuration.repository import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt similarity index 98% rename from src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt index f27012d9..279c354d 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt @@ -1,4 +1,4 @@ -package app.revanced.api.repository.backend +package app.revanced.api.configuration.repository.backend import io.ktor.client.* import kotlinx.datetime.LocalDateTime diff --git a/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt similarity index 60% rename from src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt index 1664adf3..cb22d58a 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt @@ -1,18 +1,18 @@ -package app.revanced.api.repository.backend.github +package app.revanced.api.configuration.repository.backend.github -import app.revanced.api.repository.backend.BackendRepository -import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendMember -import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor -import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease -import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset -import app.revanced.api.repository.backend.github.api.Request -import app.revanced.api.repository.backend.github.api.Request.Organization.Members -import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Contributors -import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Releases -import app.revanced.api.repository.backend.github.api.Response -import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember -import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor -import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease +import app.revanced.api.configuration.repository.backend.BackendRepository +import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendMember +import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset +import app.revanced.api.configuration.repository.backend.github.api.Request +import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Members +import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Contributors +import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Releases +import app.revanced.api.configuration.repository.backend.github.api.Response +import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubMember +import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.plugins.resources.* diff --git a/src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt similarity index 92% rename from src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt index 557f9e23..7eb98972 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt @@ -1,4 +1,4 @@ -package app.revanced.api.repository.backend.github.api +package app.revanced.api.configuration.repository.backend.github.api import io.ktor.resources.* diff --git a/src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt similarity index 95% rename from src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt index 2ddc8f1f..0128ee45 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt @@ -1,4 +1,4 @@ -package app.revanced.api.repository.backend.github.api +package app.revanced.api.configuration.repository.backend.github.api import kotlinx.datetime.Instant import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt index 8296ee3a..61e1edb1 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt @@ -1,10 +1,10 @@ package app.revanced.api.configuration.routing +import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.routing.routes.announcementsRoute import app.revanced.api.configuration.routing.routes.oldApiRoute import app.revanced.api.configuration.routing.routes.patchesRoute import app.revanced.api.configuration.routing.routes.rootRoute -import app.revanced.api.repository.ConfigurationRepository import io.ktor.server.application.* import io.ktor.server.routing.* import org.koin.ktor.ext.get diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt index bc4859d9..560e4dd6 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -1,8 +1,8 @@ package app.revanced.api.configuration.routing.routes -import app.revanced.api.schema.APIAnnouncement -import app.revanced.api.schema.APIAnnouncementArchivedAt -import app.revanced.api.services.AnnouncementService +import app.revanced.api.configuration.schema.APIAnnouncement +import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt +import app.revanced.api.configuration.services.AnnouncementService import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt index 56eb5531..762bdff2 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -1,7 +1,7 @@ package app.revanced.api.configuration.routing.routes -import app.revanced.api.services.ApiService -import app.revanced.api.services.AuthService +import app.revanced.api.configuration.services.ApiService +import app.revanced.api.configuration.services.AuthService import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt index 64f8006c..d45785f9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt @@ -1,6 +1,6 @@ package app.revanced.api.configuration.routing.routes -import app.revanced.api.repository.OldApiService +import app.revanced.api.configuration.services.OldApiService import io.ktor.server.application.* import io.ktor.server.routing.* import org.koin.ktor.ext.get diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt index 4ddfd655..6567c483 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt @@ -1,6 +1,6 @@ package app.revanced.api.configuration.routing.routes -import app.revanced.api.services.PatchesService +import app.revanced.api.configuration.services.PatchesService import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.response.* diff --git a/src/main/kotlin/app/revanced/api/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt similarity index 97% rename from src/main/kotlin/app/revanced/api/schema/APISchema.kt rename to src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index ec6355b6..86584600 100644 --- a/src/main/kotlin/app/revanced/api/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -1,4 +1,4 @@ -package app.revanced.api.schema +package app.revanced.api.configuration.schema import kotlinx.datetime.LocalDateTime import kotlinx.serialization.SerialName diff --git a/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt similarity index 80% rename from src/main/kotlin/app/revanced/api/services/AnnouncementService.kt rename to src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 63418da8..80a3ddb0 100644 --- a/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -1,8 +1,8 @@ -package app.revanced.api.services +package app.revanced.api.configuration.services -import app.revanced.api.repository.AnnouncementRepository -import app.revanced.api.schema.APIAnnouncement -import app.revanced.api.schema.APILatestAnnouncement +import app.revanced.api.configuration.repository.AnnouncementRepository +import app.revanced.api.configuration.schema.APIAnnouncement +import app.revanced.api.configuration.schema.APILatestAnnouncement import kotlinx.datetime.LocalDateTime internal class AnnouncementService( diff --git a/src/main/kotlin/app/revanced/api/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt similarity index 71% rename from src/main/kotlin/app/revanced/api/services/ApiService.kt rename to src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index d36e64b0..7e25b694 100644 --- a/src/main/kotlin/app/revanced/api/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -1,10 +1,10 @@ -package app.revanced.api.services +package app.revanced.api.configuration.services -import app.revanced.api.repository.ConfigurationRepository -import app.revanced.api.repository.backend.BackendRepository -import app.revanced.api.schema.APIContributable -import app.revanced.api.schema.APIContributor -import app.revanced.api.schema.APIMember +import app.revanced.api.configuration.repository.ConfigurationRepository +import app.revanced.api.configuration.repository.backend.BackendRepository +import app.revanced.api.configuration.schema.APIContributable +import app.revanced.api.configuration.schema.APIContributor +import app.revanced.api.configuration.schema.APIMember import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll diff --git a/src/main/kotlin/app/revanced/api/services/AuthService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt similarity index 96% rename from src/main/kotlin/app/revanced/api/services/AuthService.kt rename to src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt index 29f9a818..8e4b8a99 100644 --- a/src/main/kotlin/app/revanced/api/services/AuthService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt @@ -1,4 +1,4 @@ -package app.revanced.api.services +package app.revanced.api.configuration.services import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm diff --git a/src/main/kotlin/app/revanced/api/repository/OldApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt similarity index 98% rename from src/main/kotlin/app/revanced/api/repository/OldApiService.kt rename to src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt index 97a4443e..ea15427a 100644 --- a/src/main/kotlin/app/revanced/api/repository/OldApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt @@ -1,4 +1,4 @@ -package app.revanced.api.repository +package app.revanced.api.configuration.services import io.ktor.client.* import io.ktor.client.request.* diff --git a/src/main/kotlin/app/revanced/api/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt similarity index 88% rename from src/main/kotlin/app/revanced/api/services/PatchesService.kt rename to src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index 22afd709..2281eed3 100644 --- a/src/main/kotlin/app/revanced/api/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -1,10 +1,10 @@ -package app.revanced.api.services +package app.revanced.api.configuration.services -import app.revanced.api.repository.ConfigurationRepository -import app.revanced.api.repository.backend.BackendRepository -import app.revanced.api.schema.APIAsset -import app.revanced.api.schema.APIRelease -import app.revanced.api.schema.APIReleaseVersion +import app.revanced.api.configuration.repository.ConfigurationRepository +import app.revanced.api.configuration.repository.backend.BackendRepository +import app.revanced.api.configuration.schema.APIAsset +import app.revanced.api.configuration.schema.APIRelease +import app.revanced.api.configuration.schema.APIReleaseVersion import app.revanced.library.PatchUtils import app.revanced.patcher.PatchBundleLoader import com.github.benmanes.caffeine.cache.Caffeine From c6cacef907a5039ed029e1e26204aaba8e698aaa Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 17:29:20 +0200 Subject: [PATCH 36/81] fix: Set body for all eligible request methods --- .../configuration/services/OldApiService.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt index ea15427a..2c464aa9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt @@ -25,20 +25,21 @@ internal class OldApiService(private val client: HttpClient) { headers { appendAll( call.request.headers.filter { key, _ -> - !key.equals( - HttpHeaders.ContentType, - ignoreCase = true, - ) && - !key.equals( - HttpHeaders.ContentLength, - ignoreCase = true, - ) && - !key.equals(HttpHeaders.Host, ignoreCase = true) + !( + key.equals(HttpHeaders.ContentType, ignoreCase = true) || + key.equals(HttpHeaders.ContentLength, ignoreCase = true) || + key.equals(HttpHeaders.Host, ignoreCase = true) + ) }, ) } - if (call.request.httpMethod == HttpMethod.Post) { - body = ByteArrayContent(byteArray, call.request.contentType()) + + when (call.request.httpMethod) { + HttpMethod.Post, + HttpMethod.Put, + HttpMethod.Patch, + HttpMethod.Delete, + -> body = ByteArrayContent(byteArray, call.request.contentType()) } } From ef927688a377c16fe37b578ea870207871c30056 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 17:45:10 +0200 Subject: [PATCH 37/81] fix: Use correct proxy path --- .../app/revanced/api/configuration/routing/routes/OldApi.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt index d45785f9..fe4f5bef 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt @@ -8,7 +8,7 @@ import org.koin.ktor.ext.get internal fun Route.oldApiRoute() { val oldApiService = get() - route(Regex("(v2|tools|contributor).*")) { + route(Regex("/(v2|tools|contributors).*")) { handle { oldApiService.proxy(call) } From c23cd5cdad01fee52aecdb36b93a619886edfa4d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 5 Jun 2024 17:49:40 +0200 Subject: [PATCH 38/81] feat: Use Jetty instead of Netty --- build.gradle.kts | 7 ++++++- gradle/libs.versions.toml | 2 +- .../kotlin/app/revanced/api/command/StartAPICommand.kt | 4 ++-- src/main/resources/logback.xml | 3 +-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bc19853f..16ad9f7e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,11 @@ tasks { group = "publishing" dependsOn(startShadowScripts) } + + shadowJar { + // Needed for Jetty to work. + mergeServiceFiles() + } } application { @@ -57,7 +62,7 @@ dependencies { implementation(libs.ktor.server.cors) implementation(libs.ktor.server.caching.headers) implementation(libs.ktor.server.host.common) - implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.jetty) implementation(libs.ktor.server.conditional.headers) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.koin.ktor) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66cbed37..d800cbf6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" } ktor-server-cors = { module = "io.ktor:ktor-server-cors" } ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } -ktor-server-netty = { module = "io.ktor:ktor-server-netty" } +ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } h2 = { module = "com.h2database:h2", version.ref = "h2" } diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 2e78e652..d94e8cff 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -6,7 +6,7 @@ import app.revanced.api.configuration.configureSecurity import app.revanced.api.configuration.configureSerialization import app.revanced.api.configuration.routing.configureRouting import io.ktor.server.engine.* -import io.ktor.server.netty.* +import io.ktor.server.jetty.* import picocli.CommandLine @CommandLine.Command( @@ -29,7 +29,7 @@ internal object StartAPICommand : Runnable { private var port: Int = 8080 override fun run() { - embeddedServer(Netty, port, host) { + embeddedServer(Jetty, port, host) { configureDependencies() configureHTTP(allowedHost = host) configureSerialization() diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 3e11d781..de52ac6e 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -8,5 +8,4 @@ - - \ No newline at end of file + From 0e00d045a61d9378072113b260c41464140f90a6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 02:45:48 +0200 Subject: [PATCH 39/81] docs: Fix links --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa880ff2..6b1aadd6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,7 +70,7 @@ This document describes how to contribute to ReVanced API. ## 🙏 Submitting a feature request Features can be requested by opening an issue using the -[Feature request issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+). +[Feature request issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+). > **Note** > Requests can be accepted or rejected at the discretion of maintainers of ReVanced API. @@ -79,7 +79,7 @@ Features can be requested by opening an issue using the ## 🐞 Submitting a bug report If you encounter a bug while using ReVanced API, open an issue using the -[Bug report issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Bug+report&projects=&template=bug-report.yml&title=bug%3A+). +[Bug report issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+). ## 📝 How to contribute From 9d7b0493498bbf594928fc61181beadf6f59643b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 02:46:11 +0200 Subject: [PATCH 40/81] fix: Correct env var comment --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 9d7c0e1f..0b0ef68c 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ CONFIG_FILE_PATH=configuration.toml # Optional token for API calls to the backend BACKEND_API_TOKEN= -# An option URL to the old API to proxy for migration purposes +# A URL to the old API to proxy for migration purposes OLD_API_URL= # Database connection details From 9825865bbc7505fc3e808d21a46a151151796591 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 02:47:00 +0200 Subject: [PATCH 41/81] feat: Change default port to avoid using existing port --- docker-compose.yml | 2 +- src/main/kotlin/app/revanced/api/command/StartAPICommand.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 95b91588..6f8b392d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,5 +9,5 @@ services: environment: - COMMAND=start ports: - - localhost:8080:8080 + - localhost:8888:8888 restart: unless-stopped diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index d94e8cff..7c6c0185 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -26,7 +26,7 @@ internal object StartAPICommand : Runnable { description = ["The port to listen on."], showDefaultValue = CommandLine.Help.Visibility.ALWAYS, ) - private var port: Int = 8080 + private var port: Int = 8888 override fun run() { embeddedServer(Jetty, port, host) { From 8614e5eed685a4d44f319b4bf6f45ca8ec07e786 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 02:49:44 +0200 Subject: [PATCH 42/81] chore: Change docker compose file to example --- .gitignore | 3 ++- configuration.example.toml | 17 +++++++++++++---- ...er-compose.yml => docker-compose.example.yml | 0 3 files changed, 15 insertions(+), 5 deletions(-) rename docker-compose.yml => docker-compose.example.yml (100%) diff --git a/.gitignore b/.gitignore index 8f403a3f..083ef1ca 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ out/ ### Project ### .env persistence/ -configuration.toml \ No newline at end of file +configuration.toml +docker-compose.yml \ No newline at end of file diff --git a/configuration.example.toml b/configuration.example.toml index 5935fd61..dcef633e 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -1,5 +1,14 @@ -organization = "org" -patches-repository = "patches" -integrations-repositories = ["integrations"] -contributors-repositories = ["patches", "integrations"] +organization = "revanced" +patches-repository = "revanced-patches" +integrations-repositories = [ + "revanced-integrations" +] +contributors-repositories = [ + "revanced-patcher", + "revanced-patches", + "revanced-integrations", + "revanced-website", + "revanced-cli", + "revanced-manager", +] api-version = 1 diff --git a/docker-compose.yml b/docker-compose.example.yml similarity index 100% rename from docker-compose.yml rename to docker-compose.example.yml From 6a9f0cadac9e09bcf834ef708fab180038ebd4bd Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:36:00 +0200 Subject: [PATCH 43/81] feat: Move config file to CLI argument --- .env.example | 2 -- .../app/revanced/api/command/StartAPICommand.kt | 10 +++++++++- .../app/revanced/api/configuration/Dependencies.kt | 11 +++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 0b0ef68c..a3cb37d3 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,3 @@ -# File path to store configurations for the API -CONFIG_FILE_PATH=configuration.toml # Optional token for API calls to the backend BACKEND_API_TOKEN= # A URL to the old API to proxy for migration purposes diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 7c6c0185..f337ce54 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -8,6 +8,7 @@ import app.revanced.api.configuration.routing.configureRouting import io.ktor.server.engine.* import io.ktor.server.jetty.* import picocli.CommandLine +import java.io.File @CommandLine.Command( name = "start", @@ -28,9 +29,16 @@ internal object StartAPICommand : Runnable { ) private var port: Int = 8888 + @CommandLine.Option( + names = ["-c", "--config"], + description = ["The path to the configuration file."], + showDefaultValue = CommandLine.Help.Visibility.ALWAYS, + ) + private var configFile = File("configuration.toml") + override fun run() { embeddedServer(Jetty, port, host) { - configureDependencies() + configureDependencies(configFile) configureHTTP(allowedHost = host) configureSerialization() configureSecurity() diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 7cc14f4c..7328ea82 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -33,7 +33,9 @@ import org.koin.ktor.plugin.Koin import java.io.File @OptIn(ExperimentalSerializationApi::class) -fun Application.configureDependencies() { +fun Application.configureDependencies( + configFile: File, +) { val globalModule = module { single { Dotenv.configure().load() @@ -99,11 +101,8 @@ fun Application.configureDependencies() { ) } - single { - val configFilePath = get()["CONFIG_FILE_PATH"] - val configFile = File(configFilePath).inputStream() - - Toml.decodeFromStream(configFile) + single { + Toml.decodeFromStream(configFile.inputStream()) } singleOf(::AnnouncementRepository) From 874383e5af5ecfcacb8cedaef09b3aa740065c50 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:43:48 +0200 Subject: [PATCH 44/81] build: Sign builds --- .releaserc | 2 +- Dockerfile | 2 +- build.gradle.kts | 26 +++++++++++++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.releaserc b/.releaserc index 79200dc0..193b3fbf 100644 --- a/.releaserc +++ b/.releaserc @@ -32,7 +32,7 @@ { "assets": [ { - "path": "build/libs/*.jar" + "path": "build/libs/*" } ], successComment: false diff --git a/Dockerfile b/Dockerfile index 1dcd313a..6d1bc5a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ ENV GITHUB_TOKEN $GITHUB_TOKEN WORKDIR /app COPY . . -RUN gradle publish --no-daemon +RUN gradle startShadowScript --no-daemon # Build the runtime container FROM eclipse-temurin:latest diff --git a/build.gradle.kts b/build.gradle.kts index 16ad9f7e..7e1f8772 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.kotlin) alias(libs.plugins.ktor) alias(libs.plugins.serilization) + `maven-publish` + signing } group = "app.revanced" @@ -13,9 +15,8 @@ tasks { // Needed by gradle-semantic-release-plugin. // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - register("publish") { - group = "publishing" - dependsOn(startShadowScripts) + publish { + dependsOn(shadowJar) } shadowJar { @@ -81,3 +82,22 @@ dependencies { implementation(libs.revanced.library) implementation(libs.caffeine) } + +// The maven-publish plugin is necessary to make signing work. +publishing { + repositories { + mavenLocal() + } + + publications { + create("revanced-api-publication") { + from(components["java"]) + } + } +} + +signing { + useGpgCmd() + + sign(publishing.publications["revanced-api-publication"]) +} From 6c39353ef5ef0479050c28390ddd58713020e86d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:44:00 +0200 Subject: [PATCH 45/81] chore: Remove version --- gradle.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 65e2d640..31789604 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,3 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.0.0 \ No newline at end of file From e9badb4501263f76e57dc308563b409e83a398f9 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:44:29 +0200 Subject: [PATCH 46/81] docs: Add Docker as a method to deploy --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8b7732a5..8c830edc 100644 --- a/README.md +++ b/README.md @@ -68,24 +68,74 @@ API server for ReVanced. ## ❓ About ReVanced API is a server that is used as the backend for ReVanced. -ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) with updates and ReVanced Patches. +ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) +with updates and ReVanced Patches. ## 💪 Features Some of the features ReVanced API include: - 📢 **Announcements**: Post and get announcements grouped by channels -- ℹ️ **About**: Get more information such as a description, ways to donate to, and links of the hoster of ReVanced API +- ℹ️ **About**: Get more information such as a description, ways to donate to, +and links of the hoster of ReVanced API - 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API - 👥 **Contributors**: List all contributors involved in the project - 🔄 **Proxy**: A proxy to an old API for migration purposes and backwards compatibility ## 🚀 How to get started -1. Clone the repository -2. Set up the environment variables in a `.env` file using the `.env.example` file as a template -3. Configure the `configuration.toml` file using the `configuration.toml.example` file as a template -4. Run the server using `gradlew run --args=start` +ReVanced API can be deployed as a Docker container or used standalone. + +## 🐳 Docker + +To deploy ReVanced API as a Docker container, you can use Docker Compose or Docker CLI. +The Docker image is published on GitHub Container registry, +so before you can pull the image, you need to [authenticate to the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry). + +### 🗄️ Docker Compose + +1. Create an `.env` file using [.env.example](.env.example) as a template +2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template +3. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template +4. Run `docker-compose up -d` to start the server + +### 🐳 Docker CLI + +1. Create an `.env` file using [.env.example](.env.example) as a template +2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template +3. Start the container using the following command: + + ```shell + docker run -d --name revanced-api \ + # Mount the .env file + -v $(pwd)/.env:/app/.env \ + # Mount the configuration.toml file + -v $(pwd)/configuration.toml:/app/configuration.toml \ + # Mount the persistence folder + -v $(pwd)/persistence:/app/persistence \ + # Expose the port 8888 + -p 8888:8888 \ + ghcr.io/revanced/revanced-api:latest + ``` + +## 🖥️ Standalone + +To deploy ReVanced API standalone, you can either use the pre-built executable or build it from source. + +### 📦 Pre-built executable + +1. [Download](https://github.com/ReVanced/revanced-patches/releases/latest) ReVanced API to a folder +2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template +3. In the same folder, create a `configuration.toml` file +using [configuration.example.toml](configuration.example.toml) as a template +4. Run `java -jar revanced-api.jar start` to start the server + +### 🛠️ From source + +1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository +2. Copy [.env.example](.env.example) to `.env` and fill in the required values +3. Copy [configuration.example.toml](configuration.example.toml) to `configuration.toml` and fill in the required values +4. Run `gradlew run --args=start` to start the server ## 📚 Everything else @@ -95,14 +145,15 @@ Thank you for considering contributing to ReVanced API. You can find the contrib ### 🛠️ Building -In order to build ReVanced API, follow these steps: +In order to build ReVanced API, Java Development Kit (JDK) and Git must be installed first. +Follow the steps below to build ReVanced API: -1. Clone the repository +1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository 2. Run `gradlew build` to build the project ## 📜 Licence -ReVanced API is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information. -[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and +ReVanced API is licensed under the AGPLv3 licence. Please see the [licence file](LICENSE) for more information. +[tl;dr](https://www.tldrlegal.com/license/gnu-affero-general-public-license-v3-agpl-3-0) you may copy, distribute and modify ReVanced API as long as you track changes/dates in source files. Any modifications to ReVanced API must also be made available under the GPL along with build & install instructions. From 9e1dbb9302be923e9dfb80d9669a3c29c8163e2c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:46:34 +0200 Subject: [PATCH 47/81] chore: Change license badge to AGPL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c830edc..a71e211e 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ # 🚀 ReVanced API ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-api/release.yml) -![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg) +![AGPLv3 License](https://img.shields.io/badge/License-AGPL%20v3-yellow.svg) API server for ReVanced. From 2b0d595133078ff3f65466337943bca3a2284007 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:47:29 +0200 Subject: [PATCH 48/81] docs: Use a different icon for title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a71e211e..9e49e233 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ so before you can pull the image, you need to [authenticate to the Container reg 3. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template 4. Run `docker-compose up -d` to start the server -### 🐳 Docker CLI +### 💻 Docker CLI 1. Create an `.env` file using [.env.example](.env.example) as a template 2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template From 632001edc82db1b9e732a59f323d7441ff018e55 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:49:24 +0200 Subject: [PATCH 49/81] docs: Add missing command to Docker deployment command --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e49e233..0d34b6b3 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,6 @@ so before you can pull the image, you need to [authenticate to the Container reg 1. Create an `.env` file using [.env.example](.env.example) as a template 2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template 3. Start the container using the following command: - ```shell docker run -d --name revanced-api \ # Mount the .env file @@ -115,6 +114,9 @@ so before you can pull the image, you need to [authenticate to the Container reg -v $(pwd)/persistence:/app/persistence \ # Expose the port 8888 -p 8888:8888 \ + # Use the start command to start the server + -e COMMAND=start \ + # Pull the image from the GitHub Container registry ghcr.io/revanced/revanced-api:latest ``` From 282659d621e86d691d7be8bb35e562c1d7b6d4db Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:54:31 +0200 Subject: [PATCH 50/81] docs: Reword feature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d34b6b3..c56fb0b9 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Some of the features ReVanced API include: and links of the hoster of ReVanced API - 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API - 👥 **Contributors**: List all contributors involved in the project -- 🔄 **Proxy**: A proxy to an old API for migration purposes and backwards compatibility +- 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility ## 🚀 How to get started From 5976dfd536fcc13c824e2799ade1f7b789b11e20 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:56:04 +0200 Subject: [PATCH 51/81] docs: Fix link to correct repository --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c56fb0b9..75a37f3a 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ To deploy ReVanced API standalone, you can either use the pre-built executable o ### 📦 Pre-built executable -1. [Download](https://github.com/ReVanced/revanced-patches/releases/latest) ReVanced API to a folder +1. [Download](https://github.com/ReVanced/revanced-api/releases/latest) ReVanced API to a folder 2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template 3. In the same folder, create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template From 6b3dbab90b7e5f1e8739b6ca2a546e2caaa03012 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 03:59:29 +0200 Subject: [PATCH 52/81] docs: Add prerequisites --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75a37f3a..e520b85d 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ To deploy ReVanced API standalone, you can either use the pre-built executable o ### 📦 Pre-built executable +A Java Runtime Environment (JRE) must be installed. + 1. [Download](https://github.com/ReVanced/revanced-api/releases/latest) ReVanced API to a folder 2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template 3. In the same folder, create a `configuration.toml` file @@ -134,6 +136,8 @@ using [configuration.example.toml](configuration.example.toml) as a template ### 🛠️ From source +A Java Development Kit (JDK) and Git must be installed. + 1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository 2. Copy [.env.example](.env.example) to `.env` and fill in the required values 3. Copy [configuration.example.toml](configuration.example.toml) to `configuration.toml` and fill in the required values @@ -147,7 +151,7 @@ Thank you for considering contributing to ReVanced API. You can find the contrib ### 🛠️ Building -In order to build ReVanced API, Java Development Kit (JDK) and Git must be installed first. +To build ReVanced API, a Java Development Kit (JDK) and Git must be installed. Follow the steps below to build ReVanced API: 1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository From 71f58cf352e06b8504e00582e0c68aec55c4688b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 23:20:21 +0200 Subject: [PATCH 53/81] feat: Add GPG key to team members --- .../api/configuration/Dependencies.kt | 4 +- .../revanced/api/configuration/Extensions.kt | 7 + .../api/configuration/Serialization.kt | 1 + .../{backend => }/BackendRepository.kt | 23 +- .../repository/GitHubBackendRepository.kt | 203 ++++++++++++++++++ .../backend/github/GitHubBackendRepository.kt | 83 ------- .../repository/backend/github/api/Request.kt | 27 --- .../repository/backend/github/api/Response.kt | 52 ----- .../routing/routes/Announcements.kt | 17 +- .../api/configuration/schema/APISchema.kt | 8 +- .../api/configuration/services/ApiService.kt | 24 ++- .../configuration/services/PatchesService.kt | 2 +- 12 files changed, 261 insertions(+), 190 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/configuration/Extensions.kt rename src/main/kotlin/app/revanced/api/configuration/repository/{backend => }/BackendRepository.kt (89%) create mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 7328ea82..0fbed12d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -1,9 +1,9 @@ package app.revanced.api.configuration import app.revanced.api.configuration.repository.AnnouncementRepository +import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.repository.backend.BackendRepository -import app.revanced.api.configuration.repository.backend.github.GitHubBackendRepository +import app.revanced.api.configuration.repository.GitHubBackendRepository import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthService diff --git a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt new file mode 100644 index 00000000..e630bc68 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt @@ -0,0 +1,7 @@ +package app.revanced.api.configuration + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* + +suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound) diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt index 4e9f7ed0..43d3eab5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -13,6 +13,7 @@ fun Application.configureSerialization() { json( Json { namingStrategy = JsonNamingStrategy.SnakeCase + explicitNulls = false }, ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt similarity index 89% rename from src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt index 279c354d..a024b511 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -1,8 +1,7 @@ -package app.revanced.api.configuration.repository.backend +package app.revanced.api.configuration.repository import io.ktor.client.* import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.Serializable /** * The backend of the application used to get data for the API. @@ -40,7 +39,7 @@ abstract class BackendRepository internal constructor( * @property avatarUrl The URL to the avatar of the member. * @property url The URL to the profile of the member. * @property bio The bio of the member. - * @property gpgKeysUrl The URL to the GPG keys of the member. + * @property gpgKeys The GPG key of the member. */ @Serializable class BackendMember( @@ -48,8 +47,19 @@ abstract class BackendRepository internal constructor( override val avatarUrl: String, override val url: String, val bio: String?, - val gpgKeysUrl: String, - ) : BackendUser + val gpgKeys: GpgKeys, + ) : BackendUser { + /** + * The GPG keys of a member. + * + * @property ids The IDs of the GPG keys. + * @property url The URL to the GPG master key. + */ + class GpgKeys( + val ids: Set, + val url: String, + ) + } /** * A repository of an organization. @@ -67,7 +77,6 @@ abstract class BackendRepository internal constructor( * @property url The URL to the profile of the contributor. * @property contributions The number of contributions of the contributor. */ - @Serializable class BackendContributor( override val name: String, override val avatarUrl: String, @@ -83,7 +92,6 @@ abstract class BackendRepository internal constructor( * @property createdAt The date and time the release was created. * @property releaseNote The release note of the release. */ - @Serializable class BackendRelease( val tag: String, val releaseNote: String, @@ -95,7 +103,6 @@ abstract class BackendRepository internal constructor( * * @property downloadUrl The URL to download the asset. */ - @Serializable class BackendAsset( val downloadUrl: String, ) diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt new file mode 100644 index 00000000..8084fb0d --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt @@ -0,0 +1,203 @@ +package app.revanced.api.configuration.repository + +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendMember +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset +import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubRelease +import app.revanced.api.configuration.repository.Organization.Repository.Contributors +import app.revanced.api.configuration.repository.Organization.Repository.Releases +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.resources.* +import io.ktor.resources.* +import kotlinx.coroutines.* +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.Serializable + +class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { + override suspend fun release( + owner: String, + repository: String, + tag: String?, + ): BackendRelease { + val release: GitHubRelease = if (tag != null) { + client.get(Releases.Tag(owner, repository, tag)).body() + } else { + client.get(Releases.Latest(owner, repository)).body() + } + + return BackendRelease( + tag = release.tagName, + releaseNote = release.body, + createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), + assets = release.assets.map { + BackendAsset(downloadUrl = it.browserDownloadUrl) + }.toSet(), + ) + } + + override suspend fun contributors( + owner: String, + repository: String, + ): Set { + val contributors: Set = client.get( + Contributors( + owner, + repository, + ), + ).body() + + return contributors.map { + BackendContributor( + name = it.login, + avatarUrl = it.avatarUrl, + url = it.htmlUrl, + contributions = it.contributions, + ) + }.toSet() + } + + override suspend fun members(organization: String): Set { + // Get the list of members of the organization. + val members: Set = client.get(Organization.Members(organization)).body() + + return coroutineScope { + members.map { member -> + async { + awaitAll( + async { + // Get the user. + client.get(User(member.login)).body() + }, + async { + // Get the GPG key of the user. + client.get(User.GpgKeys(member.login)).body>() + }, + ) + } + } + }.awaitAll().map { responses -> + val user = responses[0] as GitHubUser + + @Suppress("UNCHECKED_CAST") + val gpgKeys = responses[1] as Set + + BackendMember( + name = user.login, + avatarUrl = user.avatarUrl, + url = user.htmlUrl, + bio = user.bio, + gpgKeys = + BackendMember.GpgKeys( + ids = gpgKeys.map { it.keyId }.toSet(), + url = "https://api.github.com/users/${user.login}.gpg", + ), + ) + }.toSet() + } + + override suspend fun rateLimit(): BackendRateLimit { + val rateLimit: GitHubRateLimit = client.get(RateLimit()).body() + + return BackendRateLimit( + limit = rateLimit.rate.limit, + remaining = rateLimit.rate.remaining, + reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC), + ) + } +} + +interface IGitHubUser { + val login: String + val avatarUrl: String + val htmlUrl: String +} + +@Serializable +class GitHubUser( + override val login: String, + override val avatarUrl: String, + override val htmlUrl: String, + val bio: String?, +) : IGitHubUser { + @Serializable + class GitHubGpgKey( + val keyId: String, + ) +} + +class GitHubOrganization { + @Serializable + class GitHubMember( + override val login: String, + override val avatarUrl: String, + override val htmlUrl: String, + ) : IGitHubUser + + class GitHubRepository { + @Serializable + class GitHubContributor( + override val login: String, + override val avatarUrl: String, + override val htmlUrl: String, + val contributions: Int, + ) : IGitHubUser + + @Serializable + class GitHubRelease( + val tagName: String, + val assets: Set, + val createdAt: Instant, + val body: String, + ) { + @Serializable + class GitHubAsset( + val browserDownloadUrl: String, + ) + } + } +} + +@Serializable +class GitHubRateLimit( + val rate: Rate, +) { + @Serializable + class Rate( + val limit: Int, + val remaining: Int, + val reset: Long, + ) +} + +@Resource("/users/{login}") +class User(val login: String) { + @Resource("/users/{login}/gpg_keys") + class GpgKeys(val login: String) +} + +class Organization { + @Resource("/orgs/{org}/members") + class Members(val org: String) + + class Repository { + @Resource("/repos/{owner}/{repo}/contributors") + class Contributors(val owner: String, val repo: String) + + @Resource("/repos/{owner}/{repo}/releases") + class Releases(val owner: String, val repo: String) { + @Resource("/repos/{owner}/{repo}/releases/tags/{tag}") + class Tag(val owner: String, val repo: String, val tag: String) + + @Resource("/repos/{owner}/{repo}/releases/latest") + class Latest(val owner: String, val repo: String) + } + } +} + +@Resource("/rate_limit") +class RateLimit diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt deleted file mode 100644 index cb22d58a..00000000 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.revanced.api.configuration.repository.backend.github - -import app.revanced.api.configuration.repository.backend.BackendRepository -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendMember -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset -import app.revanced.api.configuration.repository.backend.github.api.Request -import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Members -import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Contributors -import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Releases -import app.revanced.api.configuration.repository.backend.github.api.Response -import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubMember -import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor -import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.resources.* -import kotlinx.coroutines.* -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime - -class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { - override suspend fun release( - owner: String, - repository: String, - tag: String?, - ): BackendRelease { - val release: GitHubRelease = if (tag != null) { - client.get(Releases.Tag(owner, repository, tag)).body() - } else { - client.get(Releases.Latest(owner, repository)).body() - } - - return BackendRelease( - tag = release.tagName, - releaseNote = release.body, - createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), - assets = release.assets.map { - BackendAsset(downloadUrl = it.browserDownloadUrl) - }.toSet(), - ) - } - - override suspend fun contributors( - owner: String, - repository: String, - ): Set { - val contributors: Set = client.get(Contributors(owner, repository)).body() - - return contributors.map { - BackendContributor( - name = it.login, - avatarUrl = it.avatarUrl, - url = it.url, - contributions = it.contributions, - ) - }.toSet() - } - - override suspend fun members(organization: String): Set { - // Get the list of members of the organization. - val members: Set = client.get(Members(organization)).body() - - return runBlocking(Dispatchers.Default) { - members.map { member -> - // Map the member to a user in order to get the bio. - async { - client.get(Request.User(member.login)).body() - } - } - }.awaitAll().map { user -> - // Map the user back to a member. - BackendMember( - name = user.login, - avatarUrl = user.avatarUrl, - url = user.url, - bio = user.bio, - gpgKeysUrl = "https://github.com/${user.login}.gpg", - ) - }.toSet() - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt deleted file mode 100644 index 7eb98972..00000000 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.revanced.api.configuration.repository.backend.github.api - -import io.ktor.resources.* - -class Request { - @Resource("/users/{username}") - class User(val username: String) - - class Organization { - @Resource("/orgs/{org}/members") - class Members(val org: String) - - class Repository { - @Resource("/repos/{owner}/{repo}/contributors") - class Contributors(val owner: String, val repo: String) - - @Resource("/repos/{owner}/{repo}/releases") - class Releases(val owner: String, val repo: String) { - @Resource("/repos/{owner}/{repo}/releases/tags/{tag}") - class Tag(val owner: String, val repo: String, val tag: String) - - @Resource("/repos/{owner}/{repo}/releases/latest") - class Latest(val owner: String, val repo: String) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt deleted file mode 100644 index 0128ee45..00000000 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt +++ /dev/null @@ -1,52 +0,0 @@ -package app.revanced.api.configuration.repository.backend.github.api - -import kotlinx.datetime.Instant -import kotlinx.serialization.Serializable - -class Response { - interface IGitHubUser { - val login: String - val avatarUrl: String - val url: String - } - - @Serializable - class GitHubUser( - override val login: String, - override val avatarUrl: String, - override val url: String, - val bio: String?, - ) : IGitHubUser - - class GitHubOrganization { - @Serializable - class GitHubMember( - override val login: String, - override val avatarUrl: String, - override val url: String, - ) : IGitHubUser - - class GitHubRepository { - @Serializable - class GitHubContributor( - override val login: String, - override val avatarUrl: String, - override val url: String, - val contributions: Int, - ) : IGitHubUser - - @Serializable - class GitHubRelease( - val tagName: String, - val assets: Set, - val createdAt: Instant, - val body: String, - ) { - @Serializable - class GitHubAsset( - val browserDownloadUrl: String, - ) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt index 560e4dd6..0e968568 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -1,9 +1,9 @@ package app.revanced.api.configuration.routing.routes +import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt import app.revanced.api.configuration.services.AnnouncementService -import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.request.* @@ -19,17 +19,13 @@ internal fun Route.announcementsRoute() = route("announcements") { get("id") { val channel: String by call.parameters - call.respond( - announcementService.latestId(channel) ?: return@get call.respond(HttpStatusCode.NotFound), - ) + call.respondOrNotFound(announcementService.latestId(channel)) } get { val channel: String by call.parameters - call.respond( - announcementService.latest(channel) ?: return@get call.respond(HttpStatusCode.NotFound), - ) + call.respondOrNotFound(announcementService.latest(channel)) } } @@ -41,11 +37,11 @@ internal fun Route.announcementsRoute() = route("announcements") { route("latest") { get("id") { - call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) + call.respondOrNotFound(announcementService.latestId()) } get { - call.respond(announcementService.latest() ?: return@get call.respond(HttpStatusCode.NotFound)) + call.respondOrNotFound(announcementService.latest()) } } @@ -73,8 +69,9 @@ internal fun Route.announcementsRoute() = route("announcements") { patch("{id}") { val id: Int by call.parameters + val announcement = call.receive() - announcementService.update(id, call.receive()) + announcementService.update(id, announcement) } delete("{id}") { diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 86584600..11d8446b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -23,9 +23,15 @@ class APIMember( override val name: String, override val avatarUrl: String, override val url: String, - val gpgKeysUrl: String, + val gpgKey: APIGpgKey?, ) : APIUser +@Serializable +class APIGpgKey( + val id: String, + val url: String, +) + @Serializable class APIContributor( override val name: String, diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index 7e25b694..940c6e5a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -1,10 +1,8 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.repository.backend.BackendRepository -import app.revanced.api.configuration.schema.APIContributable -import app.revanced.api.configuration.schema.APIContributor -import app.revanced.api.configuration.schema.APIMember +import app.revanced.api.configuration.schema.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -27,7 +25,21 @@ internal class ApiService( } }.awaitAll() - suspend fun team() = backendRepository.members(configurationRepository.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> + APIMember( + member.name, + member.avatarUrl, + member.url, + if (member.gpgKeys.ids.isNotEmpty()) { + APIGpgKey( + // Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API. + member.gpgKeys.ids.first(), + member.gpgKeys.url, + ) + } else { + null + }, + + ) } } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index 2281eed3..591e4fe7 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -1,7 +1,7 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.repository.backend.BackendRepository import app.revanced.api.configuration.schema.APIAsset import app.revanced.api.configuration.schema.APIRelease import app.revanced.api.configuration.schema.APIReleaseVersion From b9671703be9f0842c00c38caa0953a177dc74491 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 23:21:03 +0200 Subject: [PATCH 54/81] feat: Add backend rate limit route --- .../repository/BackendRepository.kt | 21 ++++++++++++++++++- .../configuration/routing/routes/ApiRoute.kt | 5 +++++ .../api/configuration/schema/APISchema.kt | 7 +++++++ .../api/configuration/services/ApiService.kt | 4 ++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt index a024b511..b48ee81a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -41,7 +41,6 @@ abstract class BackendRepository internal constructor( * @property bio The bio of the member. * @property gpgKeys The GPG key of the member. */ - @Serializable class BackendMember( override val name: String, override val avatarUrl: String, @@ -110,6 +109,19 @@ abstract class BackendRepository internal constructor( } } + /** + * The rate limit of the backend. + * + * @property limit The limit of the rate limit. + * @property remaining The remaining requests of the rate limit. + * @property reset The date and time the rate limit resets. + */ + class BackendRateLimit( + val limit: Int, + val remaining: Int, + val reset: LocalDateTime, + ) + /** * Get a release of a repository. * @@ -140,4 +152,11 @@ abstract class BackendRepository internal constructor( * @return The members. */ abstract suspend fun members(organization: String): Set + + /** + * Get the rate limit of the backend. + * + * @return The rate limit. + */ + abstract suspend fun rateLimit(): BackendRateLimit? } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt index 762bdff2..143e14b0 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -1,5 +1,6 @@ package app.revanced.api.configuration.routing.routes +import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthService import io.ktor.http.* @@ -28,6 +29,10 @@ internal fun Route.rootRoute() { } } + get("backend/rate_limit") { + call.respondOrNotFound(apiService.rateLimit()) + } + authenticate("basic") { get("token") { call.respond(authService.newToken()) diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 11d8446b..b6018e42 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -106,3 +106,10 @@ class APILatestAnnouncement( class APIAnnouncementArchivedAt( val archivedAt: LocalDateTime, ) + +@Serializable +class APIRateLimit( + val limit: Int, + val remaining: Int, + val reset: LocalDateTime, +) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index 940c6e5a..bf408615 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -42,4 +42,8 @@ internal class ApiService( ) } + + suspend fun rateLimit() = backendRepository.rateLimit()?.let { + APIRateLimit(it.limit, it.remaining, it.reset) + } } From 80403f7130cd48e68e802ee3111760256e49c77d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 23:59:06 +0200 Subject: [PATCH 55/81] feat: Add rate limiting to routes --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- .../app/revanced/api/configuration/HTTP.kt | 14 ++- .../routing/routes/Announcements.kt | 92 ++++++++++--------- .../configuration/routing/routes/ApiRoute.kt | 37 ++++---- .../configuration/routing/routes/OldApi.kt | 9 +- .../routing/routes/PatchesRoute.kt | 19 ++-- 7 files changed, 104 insertions(+), 71 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7e1f8772..8c3226d1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,9 +62,9 @@ dependencies { implementation(libs.ktor.server.auth.jwt) implementation(libs.ktor.server.cors) implementation(libs.ktor.server.caching.headers) + implementation(libs.ktor.server.rate.limit) implementation(libs.ktor.server.host.common) implementation(libs.ktor.server.jetty) - implementation(libs.ktor.server.conditional.headers) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.koin.ktor) implementation(libs.h2) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d800cbf6..3d38a951 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,13 +20,13 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" } ktor-client-resources = { module = "io.ktor:ktor-client-resources" } ktor-client-auth = { module = "io.ktor:ktor-client-auth" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation" } -ktor-server-conditional-headers = { module = "io.ktor:ktor-server-conditional-headers" } ktor-server-core = { module = "io.ktor:ktor-server-core" } ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" } ktor-server-auth = { module = "io.ktor:ktor-server-auth" } ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" } ktor-server-cors = { module = "io.ktor:ktor-server-cors" } ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } +ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } diff --git a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt index 336b100b..d61350dc 100644 --- a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -3,15 +3,15 @@ package app.revanced.api.configuration import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.application.* +import io.ktor.server.plugins.* import io.ktor.server.plugins.cachingheaders.* -import io.ktor.server.plugins.conditionalheaders.* import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.ratelimit.* import kotlin.time.Duration.Companion.minutes fun Application.configureHTTP( allowedHost: String, ) { - install(ConditionalHeaders) install(CORS) { allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Put) @@ -23,4 +23,14 @@ fun Application.configureHTTP( install(CachingHeaders) { options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } } + install(RateLimit) { + register(RateLimitName("weak")) { + rateLimiter(limit = 30, refillPeriod = 2.minutes) + requestKey { it.request.origin.remoteAddress } + } + register(RateLimitName("strong")) { + rateLimiter(limit = 5, refillPeriod = 1.minutes) + requestKey { it.request.origin.remoteHost } + } + } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt index 0e968568..2901942b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -6,6 +6,7 @@ import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt import app.revanced.api.configuration.services.AnnouncementService import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -15,69 +16,78 @@ import org.koin.ktor.ext.get as koinGet internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() - route("{channel}/latest") { - get("id") { - val channel: String by call.parameters + rateLimit(RateLimitName("weak")) { + route("{channel}/latest") { + get("id") { + val channel: String by call.parameters - call.respondOrNotFound(announcementService.latestId(channel)) - } + call.respondOrNotFound(announcementService.latestId(channel)) + } - get { - val channel: String by call.parameters + get { + val channel: String by call.parameters - call.respondOrNotFound(announcementService.latest(channel)) + call.respondOrNotFound(announcementService.latest(channel)) + } } } - get("{channel}") { - val channel: String by call.parameters + rateLimit(RateLimitName("strong")) { + get("{channel}") { + val channel: String by call.parameters - call.respond(announcementService.all(channel)) + call.respond(announcementService.all(channel)) + } } - - route("latest") { - get("id") { - call.respondOrNotFound(announcementService.latestId()) + rateLimit(RateLimitName("strong")) { + route("latest") { + get("id") { + call.respondOrNotFound(announcementService.latestId()) + } + + get { + call.respondOrNotFound(announcementService.latest()) + } } + } + rateLimit(RateLimitName("strong")) { get { - call.respondOrNotFound(announcementService.latest()) + call.respond(announcementService.all()) } } - get { - call.respond(announcementService.all()) - } + rateLimit(RateLimitName("strong")) { + authenticate("jwt") { + post { + announcementService.new(call.receive()) + } - authenticate("jwt") { - post { - announcementService.new(call.receive()) - } + post("{id}/archive") { + val id: Int by call.parameters + val archivedAt = call.receiveNullable()?.archivedAt - post("{id}/archive") { - val id: Int by call.parameters - val archivedAt = call.receiveNullable()?.archivedAt + announcementService.archive(id, archivedAt) + } - announcementService.archive(id, archivedAt) - } + post("{id}/unarchive") { + val id: Int by call.parameters - post("{id}/unarchive") { - val id: Int by call.parameters + announcementService.unarchive(id) + } - announcementService.unarchive(id) - } - - patch("{id}") { - val id: Int by call.parameters - val announcement = call.receive() + patch("{id}") { + val id: Int by call.parameters + val announcement = call.receive() - announcementService.update(id, announcement) - } + announcementService.update(id, announcement) + } - delete("{id}") { - val id: Int by call.parameters + delete("{id}") { + val id: Int by call.parameters - announcementService.delete(id) + announcementService.delete(id) + } } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt index 143e14b0..2e1ede3c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -7,6 +7,7 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.http.content.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get @@ -15,12 +16,20 @@ internal fun Route.rootRoute() { val apiService = get() val authService = get() - get("contributors") { - call.respond(apiService.contributors()) - } + rateLimit(RateLimitName("strong")) { + authenticate("basic") { + get("token") { + call.respond(authService.newToken()) + } + } - get("team") { - call.respond(apiService.team()) + get("contributors") { + call.respond(apiService.contributors()) + } + + get("team") { + call.respond(apiService.team()) + } } route("ping") { @@ -29,18 +38,14 @@ internal fun Route.rootRoute() { } } - get("backend/rate_limit") { - call.respondOrNotFound(apiService.rateLimit()) - } - - authenticate("basic") { - get("token") { - call.respond(authService.newToken()) + rateLimit(RateLimitName("weak")) { + get("backend/rate_limit") { + call.respondOrNotFound(apiService.rateLimit()) } - } - staticResources("/", "/app/revanced/api/static") { - contentType { ContentType.Application.Json } - extensions("json") + staticResources("/", "/app/revanced/api/static") { + contentType { ContentType.Application.Json } + extensions("json") + } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt index fe4f5bef..f214f266 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt @@ -2,15 +2,18 @@ package app.revanced.api.configuration.routing.routes import app.revanced.api.configuration.services.OldApiService import io.ktor.server.application.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.routing.* import org.koin.ktor.ext.get internal fun Route.oldApiRoute() { val oldApiService = get() - route(Regex("/(v2|tools|contributors).*")) { - handle { - oldApiService.proxy(call) + rateLimit(RateLimitName("weak")) { + route(Regex("/(v2|tools|contributors).*")) { + handle { + oldApiService.proxy(call) + } } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt index 6567c483..ac3445fe 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt @@ -3,6 +3,7 @@ package app.revanced.api.configuration.routing.routes import app.revanced.api.configuration.services.PatchesService import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get as koinGet @@ -11,16 +12,20 @@ internal fun Route.patchesRoute() = route("patches") { val patchesService = koinGet() route("latest") { - get { - call.respond(patchesService.latestRelease()) - } + rateLimit(RateLimitName("weak")) { + get { + call.respond(patchesService.latestRelease()) + } - get("version") { - call.respond(patchesService.latestVersion()) + get("version") { + call.respond(patchesService.latestVersion()) + } } - get("list") { - call.respondBytes(ContentType.Application.Json) { patchesService.list() } + rateLimit(RateLimitName("strong")) { + get("list") { + call.respondBytes(ContentType.Application.Json) { patchesService.list() } + } } } } From 81cc5968d4d994b646986028cda036ab60adcc6b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 7 Jun 2024 13:55:50 +0200 Subject: [PATCH 56/81] refactor: Move files to correct folder --- .../kotlin/app/revanced/api/command/StartAPICommand.kt | 2 +- .../api/configuration/{routing => }/Routing.kt | 10 +++++----- .../{routing => }/routes/Announcements.kt | 2 +- .../api/configuration/{routing => }/routes/ApiRoute.kt | 2 +- .../api/configuration/{routing => }/routes/OldApi.kt | 2 +- .../configuration/{routing => }/routes/PatchesRoute.kt | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) rename src/main/kotlin/app/revanced/api/configuration/{routing => }/Routing.kt (61%) rename src/main/kotlin/app/revanced/api/configuration/{routing => }/routes/Announcements.kt (98%) rename src/main/kotlin/app/revanced/api/configuration/{routing => }/routes/ApiRoute.kt (96%) rename src/main/kotlin/app/revanced/api/configuration/{routing => }/routes/OldApi.kt (89%) rename src/main/kotlin/app/revanced/api/configuration/{routing => }/routes/PatchesRoute.kt (93%) diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index f337ce54..826a1ebe 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -2,9 +2,9 @@ package app.revanced.api.command import app.revanced.api.configuration.configureDependencies import app.revanced.api.configuration.configureHTTP +import app.revanced.api.configuration.configureRouting import app.revanced.api.configuration.configureSecurity import app.revanced.api.configuration.configureSerialization -import app.revanced.api.configuration.routing.configureRouting import io.ktor.server.engine.* import io.ktor.server.jetty.* import picocli.CommandLine diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt similarity index 61% rename from src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt rename to src/main/kotlin/app/revanced/api/configuration/Routing.kt index 61e1edb1..1aee5227 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -1,10 +1,10 @@ -package app.revanced.api.configuration.routing +package app.revanced.api.configuration import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.routing.routes.announcementsRoute -import app.revanced.api.configuration.routing.routes.oldApiRoute -import app.revanced.api.configuration.routing.routes.patchesRoute -import app.revanced.api.configuration.routing.routes.rootRoute +import app.revanced.api.configuration.routes.announcementsRoute +import app.revanced.api.configuration.routes.oldApiRoute +import app.revanced.api.configuration.routes.patchesRoute +import app.revanced.api.configuration.routes.rootRoute import io.ktor.server.application.* import io.ktor.server.routing.* import org.koin.ktor.ext.get diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt similarity index 98% rename from src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt rename to src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 2901942b..7630b6b6 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -1,4 +1,4 @@ -package app.revanced.api.configuration.routing.routes +package app.revanced.api.configuration.routes import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.schema.APIAnnouncement diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt similarity index 96% rename from src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt rename to src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 2e1ede3c..3271b810 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -1,4 +1,4 @@ -package app.revanced.api.configuration.routing.routes +package app.revanced.api.configuration.routes import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.services.ApiService diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt similarity index 89% rename from src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt rename to src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt index f214f266..e72f470e 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt @@ -1,4 +1,4 @@ -package app.revanced.api.configuration.routing.routes +package app.revanced.api.configuration.routes import app.revanced.api.configuration.services.OldApiService import io.ktor.server.application.* diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt similarity index 93% rename from src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt rename to src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index ac3445fe..a5811bfb 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -1,4 +1,4 @@ -package app.revanced.api.configuration.routing.routes +package app.revanced.api.configuration.routes import app.revanced.api.configuration.services.PatchesService import io.ktor.http.* From 205bcde77aad90e0eb49fc25961399f1e37698bb Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 8 Jun 2024 03:41:48 +0200 Subject: [PATCH 57/81] feat: Setup cors and cache --- configuration.example.toml | 1 + configuration.toml | 1 + .../revanced/api/command/StartAPICommand.kt | 2 +- .../app/revanced/api/configuration/HTTP.kt | 22 ++++------ .../app/revanced/api/configuration/Routing.kt | 12 ++++++ .../repository/ConfigurationRepository.kt | 1 + .../api/configuration/routes/Announcements.kt | 13 ++++++ .../api/configuration/routes/ApiRoute.kt | 40 +++++++++++++++---- 8 files changed, 69 insertions(+), 23 deletions(-) diff --git a/configuration.example.toml b/configuration.example.toml index dcef633e..e969f57d 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -12,3 +12,4 @@ contributors-repositories = [ "revanced-manager", ] api-version = 1 +host = "*.revanced.app" diff --git a/configuration.toml b/configuration.toml index dcef633e..044b6e4f 100644 --- a/configuration.toml +++ b/configuration.toml @@ -12,3 +12,4 @@ contributors-repositories = [ "revanced-manager", ] api-version = 1 +host = "*.revanced.app" \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 826a1ebe..7131be4c 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -39,7 +39,7 @@ internal object StartAPICommand : Runnable { override fun run() { embeddedServer(Jetty, port, host) { configureDependencies(configFile) - configureHTTP(allowedHost = host) + configureHTTP() configureSerialization() configureSecurity() configureRouting() diff --git a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt index d61350dc..09811a12 100644 --- a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -1,28 +1,20 @@ package app.revanced.api.configuration -import io.ktor.http.* -import io.ktor.http.content.* +import app.revanced.api.configuration.repository.ConfigurationRepository import io.ktor.server.application.* import io.ktor.server.plugins.* -import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.ratelimit.* +import org.koin.ktor.ext.get import kotlin.time.Duration.Companion.minutes -fun Application.configureHTTP( - allowedHost: String, -) { +fun Application.configureHTTP() { + val configurationRepository = get() + install(CORS) { - allowMethod(HttpMethod.Options) - allowMethod(HttpMethod.Put) - allowMethod(HttpMethod.Delete) - allowMethod(HttpMethod.Patch) - allowHeader(HttpHeaders.Authorization) - allowHost(allowedHost) - } - install(CachingHeaders) { - options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } + allowHost(host = configurationRepository.host) } + install(RateLimit) { register(RateLimitName("weak")) { rateLimiter(limit = 30, refillPeriod = 2.minutes) diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index 1aee5227..d6dcff88 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -5,13 +5,25 @@ import app.revanced.api.configuration.routes.announcementsRoute import app.revanced.api.configuration.routes.oldApiRoute import app.revanced.api.configuration.routes.patchesRoute import app.revanced.api.configuration.routes.rootRoute +import io.ktor.http.* +import io.ktor.http.content.* import io.ktor.server.application.* +import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.routing.* import org.koin.ktor.ext.get +import kotlin.time.Duration.Companion.minutes internal fun Application.configureRouting() = routing { val configuration = get() + install(CachingHeaders) { + options { _, _ -> + CachingOptions( + CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt()), + ) + } + } + route("/v${configuration.apiVersion}") { rootRoute() patchesRoute() diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 3d5f28c1..a7a37023 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -14,4 +14,5 @@ internal class ConfigurationRepository( val contributorsRepositoryNames: Set, @SerialName("api-version") val apiVersion: Int = 1, + val host: String, ) diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 7630b6b6..984eb178 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -4,18 +4,30 @@ import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt import app.revanced.api.configuration.services.AnnouncementService +import io.ktor.http.* +import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.util.* +import kotlin.time.Duration.Companion.minutes import org.koin.ktor.ext.get as koinGet internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() + install(CachingHeaders) { + options { _, _ -> + CachingOptions( + CacheControl.MaxAge(maxAgeSeconds = 1.minutes.inWholeSeconds.toInt()), + ) + } + } + rateLimit(RateLimitName("weak")) { route("{channel}/latest") { get("id") { @@ -39,6 +51,7 @@ internal fun Route.announcementsRoute() = route("announcements") { call.respond(announcementService.all(channel)) } } + rateLimit(RateLimitName("strong")) { route("latest") { get("id") { diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 3271b810..2b20b0b5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -4,17 +4,21 @@ import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthService import io.ktor.http.* +import io.ktor.http.content.CachingOptions import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.http.content.* +import io.ktor.server.plugins.cachingheaders.* +import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* -import org.koin.ktor.ext.get +import kotlin.time.Duration.Companion.days +import org.koin.ktor.ext.get as koinGet internal fun Route.rootRoute() { - val apiService = get() - val authService = get() + val apiService = koinGet() + val authService = koinGet() rateLimit(RateLimitName("strong")) { authenticate("basic") { @@ -23,16 +27,38 @@ internal fun Route.rootRoute() { } } - get("contributors") { - call.respond(apiService.contributors()) + route("contributors") { + install(CachingHeaders) { + options { _, _ -> + CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt())) + } + } + + get { + call.respond(apiService.contributors()) + } } - get("team") { - call.respond(apiService.team()) + route("team") { + install(CachingHeaders) { + options { _, _ -> + CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt())) + } + } + + get { + call.respond(apiService.team()) + } } } route("ping") { + install(CachingHeaders) { + options { _, _ -> + CachingOptions(CacheControl.NoCache(null)) + } + } + handle { call.respond(HttpStatusCode.NoContent) } From 6ea63be490e7786c6486ee78c1fa38f302e8b81c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 9 Jun 2024 01:28:33 +0200 Subject: [PATCH 58/81] feat: Add OpenAPI docs and cache to routes --- build.gradle.kts | 1 + .../app/revanced/api/command/MainCommand.kt | 18 +- .../revanced/api/command/StartAPICommand.kt | 7 +- .../revanced/api/configuration/Extensions.kt | 22 +- .../app/revanced/api/configuration/OpenAPI.kt | 52 +++ .../app/revanced/api/configuration/Routing.kt | 11 +- .../api/configuration/Serialization.kt | 2 + .../repository/AnnouncementRepository.kt | 8 +- .../api/configuration/routes/Announcements.kt | 350 +++++++++++++++--- .../api/configuration/routes/ApiRoute.kt | 126 +++++-- .../api/configuration/routes/PatchesRoute.kt | 67 +++- .../api/configuration/schema/APISchema.kt | 2 +- .../services/AnnouncementService.kt | 6 +- .../api/configuration/services/ApiService.kt | 4 +- 14 files changed, 572 insertions(+), 104 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8c3226d1..7a421557 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(libs.ktor.server.jetty) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.koin.ktor) + implementation("io.bkbn:kompendium-core:latest.release") implementation(libs.h2) implementation(libs.logback.classic) implementation(libs.exposed.core) diff --git a/src/main/kotlin/app/revanced/api/command/MainCommand.kt b/src/main/kotlin/app/revanced/api/command/MainCommand.kt index 6b773879..c665597b 100644 --- a/src/main/kotlin/app/revanced/api/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/api/command/MainCommand.kt @@ -3,6 +3,14 @@ package app.revanced.api.command import picocli.CommandLine import java.util.* +internal val applicationVersion = MainCommand::class.java.getResourceAsStream( + "/app/revanced/api/version.properties", +)?.use { stream -> + Properties().apply { + load(stream) + }.getProperty("version") +} ?: "v0.0.0" + fun main(args: Array) { CommandLine(MainCommand).execute(*args).let(System::exit) } @@ -10,15 +18,7 @@ fun main(args: Array) { private object CLIVersionProvider : CommandLine.IVersionProvider { override fun getVersion() = arrayOf( - MainCommand::class.java.getResourceAsStream( - "/app/revanced/api/version.properties", - )?.use { stream -> - Properties().apply { - load(stream) - }.let { - "ReVanced API v${it.getProperty("version")}" - } - } ?: "ReVanced API", + "ReVanced API $applicationVersion", ) } diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 7131be4c..a220addb 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -1,10 +1,6 @@ package app.revanced.api.command -import app.revanced.api.configuration.configureDependencies -import app.revanced.api.configuration.configureHTTP -import app.revanced.api.configuration.configureRouting -import app.revanced.api.configuration.configureSecurity -import app.revanced.api.configuration.configureSerialization +import app.revanced.api.configuration.* import io.ktor.server.engine.* import io.ktor.server.jetty.* import picocli.CommandLine @@ -42,6 +38,7 @@ internal object StartAPICommand : Runnable { configureHTTP() configureSerialization() configureSecurity() + configureOpenAPI() configureRouting() }.start(wait = true) } diff --git a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt index e630bc68..d7546939 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt @@ -1,7 +1,27 @@ package app.revanced.api.configuration +import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.ktor.http.* +import io.ktor.http.content.* import io.ktor.server.application.* +import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.response.* +import kotlin.time.Duration -suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound) +internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound) + +internal fun ApplicationCallPipeline.installCache(maxAge: Duration) = + installCache(CacheControl.MaxAge(maxAgeSeconds = maxAge.inWholeSeconds.toInt())) + +internal fun ApplicationCallPipeline.installNoCache() = + installCache(CacheControl.NoCache(null)) + +internal fun ApplicationCallPipeline.installCache(cacheControl: CacheControl) = + install(CachingHeaders) { + options { _, _ -> + CachingOptions(cacheControl) + } + } + +internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) = + install(NotarizedRoute(), configure) diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt new file mode 100644 index 00000000..48bcb881 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -0,0 +1,52 @@ +package app.revanced.api.configuration + +import app.revanced.api.command.applicationVersion +import io.bkbn.kompendium.core.plugin.NotarizedApplication +import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator +import io.bkbn.kompendium.oas.OpenApiSpec +import io.bkbn.kompendium.oas.component.Components +import io.bkbn.kompendium.oas.info.Contact +import io.bkbn.kompendium.oas.info.Info +import io.bkbn.kompendium.oas.info.License +import io.bkbn.kompendium.oas.security.BasicAuth +import io.bkbn.kompendium.oas.security.BearerAuth +import io.bkbn.kompendium.oas.server.Server +import io.ktor.server.application.* +import java.net.URI + +internal fun Application.configureOpenAPI() { + install(NotarizedApplication()) { + spec = { + OpenApiSpec( + info = Info( + title = "Revanced API", + version = applicationVersion, + description = "API server for ReVanced.", + contact = Contact( + name = "ReVanced", + url = URI("https://revanced.app"), + email = "contact@revanced.app", + ), + license = License( + name = "AGPLv3", + url = URI("https://github.com/ReVanced/revanced-api/blob/main/LICENSE"), + ), + ), + components = Components( + securitySchemes = mutableMapOf( + "bearer" to BearerAuth(), + "basic" to BasicAuth(), + ), + ), + + ).apply { + servers += Server( + url = URI("https://api.revanced.app"), + description = "ReVanced API server.", + ) + } + } + + schemaConfigurator = KotlinXSchemaConfigurator() + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index d6dcff88..f20dc4c4 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -5,10 +5,7 @@ import app.revanced.api.configuration.routes.announcementsRoute import app.revanced.api.configuration.routes.oldApiRoute import app.revanced.api.configuration.routes.patchesRoute import app.revanced.api.configuration.routes.rootRoute -import io.ktor.http.* -import io.ktor.http.content.* import io.ktor.server.application.* -import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.routing.* import org.koin.ktor.ext.get import kotlin.time.Duration.Companion.minutes @@ -16,13 +13,7 @@ import kotlin.time.Duration.Companion.minutes internal fun Application.configureRouting() = routing { val configuration = get() - install(CachingHeaders) { - options { _, _ -> - CachingOptions( - CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt()), - ) - } - } + installCache(5.minutes) route("/v${configuration.apiVersion}") { rootRoute() diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt index 43d3eab5..ceb6182c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -1,5 +1,6 @@ package app.revanced.api.configuration +import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* @@ -12,6 +13,7 @@ fun Application.configureSerialization() { install(ContentNegotiation) { json( Json { + serializersModule = KompendiumSerializersModule.module namingStrategy = JsonNamingStrategy.SnakeCase explicitNulls = false }, diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index 7da8300e..eb8475e1 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -2,8 +2,8 @@ package app.revanced.api.configuration.repository import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement import app.revanced.api.configuration.schema.APIAnnouncement -import app.revanced.api.configuration.schema.APILatestAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.datetime.* import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass @@ -42,6 +42,8 @@ internal class AnnouncementRepository(private val database: Database) { announcement.delete() } + // TODO: These are inefficient, but I'm not sure how to make them more efficient. + fun latest() = transaction { AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() } @@ -52,13 +54,13 @@ internal class AnnouncementRepository(private val database: Database) { fun latestId() = transaction { AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { - APILatestAnnouncement(it) + APIResponseAnnouncementId(it) } } fun latestId(channel: String) = transaction { AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { - APILatestAnnouncement(it) + APIResponseAnnouncementId(it) } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 984eb178..814402c4 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -1,14 +1,22 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.installCache +import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt +import app.revanced.api.configuration.schema.APIResponseAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncementId import app.revanced.api.configuration.services.AnnouncementService +import io.bkbn.kompendium.core.metadata.DeleteInfo +import io.bkbn.kompendium.core.metadata.GetInfo +import io.bkbn.kompendium.core.metadata.PatchInfo +import io.bkbn.kompendium.core.metadata.PostInfo +import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Parameter import io.ktor.http.* -import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.auth.* -import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.request.* import io.ktor.server.response.* @@ -20,87 +28,343 @@ import org.koin.ktor.ext.get as koinGet internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() - install(CachingHeaders) { - options { _, _ -> - CachingOptions( - CacheControl.MaxAge(maxAgeSeconds = 1.minutes.inWholeSeconds.toInt()), - ) + installCache(5.minutes) + + installAnnouncementsRouteDocumentation() + + rateLimit(RateLimitName("strong")) { + get { + call.respond(announcementService.all()) } } - rateLimit(RateLimitName("weak")) { + rateLimit(RateLimitName("strong")) { route("{channel}/latest") { - get("id") { - val channel: String by call.parameters - - call.respondOrNotFound(announcementService.latestId(channel)) - } + installLatestChannelAnnouncementRouteDocumentation() get { val channel: String by call.parameters call.respondOrNotFound(announcementService.latest(channel)) } + + route("id") { + installLatestChannelAnnouncementIdRouteDocumentation() + + get { + val channel: String by call.parameters + + call.respondOrNotFound(announcementService.latestId(channel)) + } + } } } rateLimit(RateLimitName("strong")) { - get("{channel}") { - val channel: String by call.parameters + route("{channel}") { + installChannelAnnouncementsRouteDocumentation() + + get { + val channel: String by call.parameters - call.respond(announcementService.all(channel)) + call.respond(announcementService.all(channel)) + } } } rateLimit(RateLimitName("strong")) { route("latest") { - get("id") { - call.respondOrNotFound(announcementService.latestId()) - } + installLatestAnnouncementRouteDocumentation() get { call.respondOrNotFound(announcementService.latest()) } - } - } - rateLimit(RateLimitName("strong")) { - get { - call.respond(announcementService.all()) + route("id") { + installLatestAnnouncementIdRouteDocumentation() + + get { + call.respondOrNotFound(announcementService.latestId()) + } + } } } rateLimit(RateLimitName("strong")) { authenticate("jwt") { - post { - announcementService.new(call.receive()) + post { announcement -> + announcementService.new(announcement) } - post("{id}/archive") { - val id: Int by call.parameters - val archivedAt = call.receiveNullable()?.archivedAt + route("{id}") { + installAnnouncementIdRouteDocumentation() - announcementService.archive(id, archivedAt) - } + patch { announcement -> + val id: Int by call.parameters - post("{id}/unarchive") { - val id: Int by call.parameters + announcementService.update(id, announcement) + } - announcementService.unarchive(id) - } + delete { + val id: Int by call.parameters - patch("{id}") { - val id: Int by call.parameters - val announcement = call.receive() + announcementService.delete(id) + } - announcementService.update(id, announcement) - } + route("archive") { + installAnnouncementArchiveRouteDocumentation() + + post { + val id: Int by call.parameters + val archivedAt = call.receiveNullable()?.archivedAt - delete("{id}") { - val id: Int by call.parameters + announcementService.archive(id, archivedAt) + } + } - announcementService.delete(id) + route("unarchive") { + installAnnouncementUnarchiveRouteDocumentation() + + post { + val id: Int by call.parameters + + announcementService.unarchive(id) + } + } } } } } + +private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + get = GetInfo.builder { + description("Get the latest announcement") + summary("Get latest announcement") + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The latest announcement") + responseType() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("No announcement exists") + responseType() + } + } +} + +private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + get = GetInfo.builder { + description("Get the id of the latest announcement") + summary("Get id of latest announcement") + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The id of the latest announcement") + responseType() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("No announcement exists") + responseType() + } + } +} + +private fun Route.installChannelAnnouncementsRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + parameters = listOf( + Parameter( + name = "channel", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING, + description = "The channel to get the announcements from", + required = true, + ), + ) + + get = GetInfo.builder { + description("Get the announcements from a channel") + summary("Get announcements from channel") + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The announcements in the channel") + responseType>() + } + } +} + +private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.INT, + description = "The id of the announcement to archive", + required = true, + ), + Parameter( + name = "archivedAt", + `in` = Parameter.Location.query, + schema = TypeDefinition.STRING, + description = "The date and time the announcement to be archived", + required = false, + ), + ) + + post = PostInfo.builder { + description("Archive an announcement") + summary("Archive announcement") + response { + description("When the announcement was archived") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.INT, + description = "The id of the announcement to unarchive", + required = true, + ), + ) + + post = PostInfo.builder { + description("Unarchive an announcement") + summary("Unarchive announcement") + response { + description("When announcement was unarchived") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.INT, + description = "The id of the announcement to update", + required = true, + ), + ) + + patch = PatchInfo.builder { + description("Update an announcement") + summary("Update announcement") + request { + requestType() + description("The new announcement") + } + response { + description("When announcement was updated") + responseCode(HttpStatusCode.OK) + responseType() + } + } + + delete = DeleteInfo.builder { + description("Delete an announcement") + summary("Delete announcement") + response { + description("When the announcement was deleted") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + get = GetInfo.builder { + description("Get the announcements") + summary("Get announcement") + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The announcements") + responseType>() + } + } +} + +private fun Route.installLatestChannelAnnouncementRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + parameters = listOf( + Parameter( + name = "channel", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING, + description = "The channel to get the latest announcement from", + required = true, + ), + ) + + get = GetInfo.builder { + description("Get the latest announcement from a channel") + summary("Get latest channel announcement") + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The latest announcement in the channel") + responseType() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("The channel does not exist") + responseType() + } + } +} + +private fun Route.installLatestChannelAnnouncementIdRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + parameters = listOf( + Parameter( + name = "channel", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING, + description = "The channel to get the latest announcement id from", + required = true, + ), + ) + + get = GetInfo.builder { + description("Get the id of the latest announcement from a channel") + summary("Get id of latest announcement from channel") + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The id of the latest announcement from the channel") + responseType() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("The channel does not exist") + responseType() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 2b20b0b5..fe2d3302 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -1,15 +1,19 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.installCache +import app.revanced.api.configuration.installNoCache +import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.respondOrNotFound +import app.revanced.api.configuration.schema.APIContributable +import app.revanced.api.configuration.schema.APIMember +import app.revanced.api.configuration.schema.APIRateLimit import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthService +import io.bkbn.kompendium.core.metadata.* import io.ktor.http.* -import io.ktor.http.content.CachingOptions import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.http.content.* -import io.ktor.server.plugins.cachingheaders.* -import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -22,17 +26,19 @@ internal fun Route.rootRoute() { rateLimit(RateLimitName("strong")) { authenticate("basic") { - get("token") { - call.respond(authService.newToken()) + route("token") { + installTokenRouteDocumentation() + + get { + call.respond(authService.newToken()) + } } } route("contributors") { - install(CachingHeaders) { - options { _, _ -> - CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt())) - } - } + installCache(1.days) + + installContributorsRouteDocumentation() get { call.respond(apiService.contributors()) @@ -40,11 +46,9 @@ internal fun Route.rootRoute() { } route("team") { - install(CachingHeaders) { - options { _, _ -> - CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt())) - } - } + installCache(1.days) + + installTeamRouteDocumentation() get { call.respond(apiService.team()) @@ -53,20 +57,22 @@ internal fun Route.rootRoute() { } route("ping") { - install(CachingHeaders) { - options { _, _ -> - CachingOptions(CacheControl.NoCache(null)) - } - } + installNoCache() - handle { + installPingRouteDocumentation() + + head { call.respond(HttpStatusCode.NoContent) } } rateLimit(RateLimitName("weak")) { - get("backend/rate_limit") { - call.respondOrNotFound(apiService.rateLimit()) + route("backend/rate_limit") { + installRateLimitRouteDocumentation() + + get { + call.respondOrNotFound(apiService.rateLimit()) + } } staticResources("/", "/app/revanced/api/static") { @@ -75,3 +81,77 @@ internal fun Route.rootRoute() { } } } + +fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute { + tags = setOf("API") + + get = GetInfo.builder { + description("Get the rate limit of the backend") + summary("Get rate limit of backend") + response { + description("The rate limit of the backend") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +fun Route.installPingRouteDocumentation() = installNotarizedRoute { + tags = setOf("API") + + head = HeadInfo.builder { + description("Ping the server") + summary("Ping") + response { + description("The server is reachable") + responseCode(HttpStatusCode.NoContent) + responseType() + } + } +} + +fun Route.installTeamRouteDocumentation() = installNotarizedRoute { + tags = setOf("API") + + get = GetInfo.builder { + description("Get the list of team members") + summary("Get team members") + response { + description("The list of team members") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType>() + } + } +} + +fun Route.installContributorsRouteDocumentation() = installNotarizedRoute { + tags = setOf("API") + + get = GetInfo.builder { + description("Get the list of contributors") + summary("Get contributors") + response { + description("The list of contributors") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType>() + } + } +} + +fun Route.installTokenRouteDocumentation() = installNotarizedRoute { + tags = setOf("API") + + get = GetInfo.builder { + description("Get a new authorization token") + summary("Get authorization token") + response { + description("The authorization token") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index a5811bfb..ab11b324 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -1,6 +1,10 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.installNotarizedRoute +import app.revanced.api.configuration.schema.APIRelease +import app.revanced.api.configuration.schema.APIReleaseVersion import app.revanced.api.configuration.services.PatchesService +import io.bkbn.kompendium.core.metadata.GetInfo import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.ratelimit.* @@ -12,20 +16,75 @@ internal fun Route.patchesRoute() = route("patches") { val patchesService = koinGet() route("latest") { + installLatestPatchesRouteDocumentation() + rateLimit(RateLimitName("weak")) { get { call.respond(patchesService.latestRelease()) } - get("version") { - call.respond(patchesService.latestVersion()) + route("version") { + installLatestPatchesVersionRouteDocumentation() + + get { + call.respond(patchesService.latestVersion()) + } } } rateLimit(RateLimitName("strong")) { - get("list") { - call.respondBytes(ContentType.Application.Json) { patchesService.list() } + route("list") { + installLatestPatchesListRouteDocumentation() + + get { + call.respondBytes(ContentType.Application.Json) { patchesService.list() } + } } } } } + +fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute { + tags = setOf("Patches") + + get = GetInfo.builder { + description("Get the latest patches release") + summary("Get latest patches release") + response { + description("The latest patches release") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute { + tags = setOf("Patches") + + get = GetInfo.builder { + description("Get the latest patches release version") + summary("Get latest patches release version") + response { + description("The latest patches release version") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute { + tags = setOf("Patches") + + get = GetInfo.builder { + description("Get the list of patches from the latest patches release") + summary("Get list of patches from latest patches release") + response { + description("The list of patches") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index b6018e42..1dad95fa 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -98,7 +98,7 @@ class APIResponseAnnouncement( ) @Serializable -class APILatestAnnouncement( +class APIResponseAnnouncementId( val id: Int, ) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 80a3ddb0..7a0b4603 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -2,14 +2,14 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.AnnouncementRepository import app.revanced.api.configuration.schema.APIAnnouncement -import app.revanced.api.configuration.schema.APILatestAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.datetime.LocalDateTime internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - fun latestId(channel: String): APILatestAnnouncement? = announcementRepository.latestId(channel) - fun latestId(): APILatestAnnouncement? = announcementRepository.latestId() + fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) + fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() fun latest(channel: String) = announcementRepository.latest(channel) fun latest() = announcementRepository.latest() diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index bf408615..10b54cf3 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -23,7 +23,7 @@ internal class ApiService( ) } } - }.awaitAll() + }.awaitAll().toSet() suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> APIMember( @@ -41,7 +41,7 @@ internal class ApiService( }, ) - } + }.toSet() suspend fun rateLimit() = backendRepository.rateLimit()?.let { APIRateLimit(it.limit, it.remaining, it.reset) From 17ecf58e550d13dd93ab69e1cf522366aeb3da3f Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 9 Jun 2024 02:01:11 +0200 Subject: [PATCH 59/81] fix: Fix spelling mistake --- src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt | 2 +- .../app/revanced/api/configuration/routes/Announcements.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index 48bcb881..0719ebce 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -19,7 +19,7 @@ internal fun Application.configureOpenAPI() { spec = { OpenApiSpec( info = Info( - title = "Revanced API", + title = "ReVanced API", version = applicationVersion, description = "API server for ReVanced.", contact = Contact( diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 814402c4..70a6bc01 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -299,7 +299,7 @@ private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRou get = GetInfo.builder { description("Get the announcements") - summary("Get announcement") + summary("Get announcements") response { responseCode(HttpStatusCode.OK) mediaTypes("application/json") From cd5d57f8f87125df361e23715eda6e755203d727 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 9 Jun 2024 02:03:05 +0200 Subject: [PATCH 60/81] feat: Add local ReVanced API server --- src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index 0719ebce..4c02028f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -44,6 +44,11 @@ internal fun Application.configureOpenAPI() { url = URI("https://api.revanced.app"), description = "ReVanced API server.", ) + + servers += Server( + url = URI("http://localhost:8888"), + description = "Local ReVanced API server.", + ) } } From f9cae1ea56c93aded25159f6b0814bf84d192192 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 9 Jun 2024 02:04:45 +0200 Subject: [PATCH 61/81] fix: Remove punctuation --- src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index 4c02028f..1dd1fa74 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -42,12 +42,12 @@ internal fun Application.configureOpenAPI() { ).apply { servers += Server( url = URI("https://api.revanced.app"), - description = "ReVanced API server.", + description = "ReVanced API server", ) servers += Server( url = URI("http://localhost:8888"), - description = "Local ReVanced API server.", + description = "Local ReVanced API server", ) } } From 4a685a2b538492238421e29f522573604634224a Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 29 Jun 2024 03:44:55 +0200 Subject: [PATCH 62/81] Send signatures and verify signature of patches file before loading it --- .github/workflows/release.yml | 3 + build.gradle.kts | 5 +- configuration.example.toml | 6 +- configuration.toml | 6 +- docker-compose.example.yml | 2 + gradle/libs.versions.toml | 8 +- .../revanced/api/command/StartAPICommand.kt | 1 + .../api/configuration/Dependencies.kt | 2 + .../app/revanced/api/configuration/Logging.kt | 16 +++ .../repository/BackendRepository.kt | 8 +- .../repository/ConfigurationRepository.kt | 70 +++++++++++- .../repository/GitHubBackendRepository.kt | 6 +- .../api/configuration/routes/PatchesRoute.kt | 30 +++++ .../api/configuration/schema/APISchema.kt | 35 +++--- .../configuration/services/PatchesService.kt | 104 ++++++++++++------ .../services/SignatureService.kt | 72 ++++++++++++ src/main/resources/logback.xml | 3 +- 17 files changed, 303 insertions(+), 74 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/configuration/Logging.kt create mode 100644 src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0074423..2a4601fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,3 +81,6 @@ jobs: push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} + build-args: | + GITHUB_ACTOR=${{ github.actor }} + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} diff --git a/build.gradle.kts b/build.gradle.kts index 7a421557..de068fe8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,9 +65,10 @@ dependencies { implementation(libs.ktor.server.rate.limit) implementation(libs.ktor.server.host.common) implementation(libs.ktor.server.jetty) + implementation(libs.ktor.server.call.logging) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.koin.ktor) - implementation("io.bkbn:kompendium-core:latest.release") + implementation(libs.kompendium.core) implementation(libs.h2) implementation(libs.logback.classic) implementation(libs.exposed.core) @@ -82,6 +83,8 @@ dependencies { implementation(libs.revanced.patcher) implementation(libs.revanced.library) implementation(libs.caffeine) + implementation(libs.bouncy.castle.provider) + implementation(libs.bouncy.castle.pgp) } // The maven-publish plugin is necessary to make signing work. diff --git a/configuration.example.toml b/configuration.example.toml index e969f57d..1530f04f 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -1,8 +1,6 @@ organization = "revanced" -patches-repository = "revanced-patches" -integrations-repositories = [ - "revanced-integrations" -] +patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" } +integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" } contributors-repositories = [ "revanced-patcher", "revanced-patches", diff --git a/configuration.toml b/configuration.toml index 044b6e4f..28155952 100644 --- a/configuration.toml +++ b/configuration.toml @@ -1,8 +1,6 @@ organization = "revanced" -patches-repository = "revanced-patches" -integrations-repositories = [ - "revanced-integrations" -] +patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" } +integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" } contributors-repositories = [ "revanced-patcher", "revanced-patches", diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 6f8b392d..c8c032aa 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -6,6 +6,8 @@ services: - /data/revanced-api/persistence:/app/persistence - /data/revanced-api/.env:/app/.env - /data/revanced-api/configuration.toml:/app/configuration.toml + - /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc + - /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc environment: - COMMAND=start ports: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d38a951..cf646231 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +kompendium-core = "latest.release" kotlin = "2.0.0" logback = "1.4.14" exposed = "0.41.1" @@ -7,13 +8,15 @@ koin = "3.5.3" dotenv = "6.4.1" ktor = "2.3.7" ktoml = "0.5.1" -picocli = "4.7.5" +picocli = "4.7.6" datetime = "0.5.0" revanced-patcher = "19.3.1" revanced-library = "2.3.0" caffeine = "3.1.8" +bouncy-castle = "1.78.1" [libraries] +kompendium-core = { module = "io.bkbn:kompendium-core", version.ref = "kompendium-core" } ktor-client-core = { module = "io.ktor:ktor-client-core" } ktor-client-cio = { module = "io.ktor:ktor-client-cio" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" } @@ -29,6 +32,7 @@ ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" } +ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } h2 = { module = "com.h2database:h2", version.ref = "h2" } @@ -45,6 +49,8 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } +bouncy-castle-provider = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncy-castle" } +bouncy-castle-pgp = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncy-castle" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index a220addb..66faa070 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -39,6 +39,7 @@ internal object StartAPICommand : Runnable { configureSerialization() configureSecurity() configureOpenAPI() + configureLogging() configureRouting() }.start(wait = true) } diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 0fbed12d..24ebb542 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -4,6 +4,7 @@ import app.revanced.api.configuration.repository.AnnouncementRepository import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.GitHubBackendRepository +import app.revanced.api.configuration.services.* import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthService @@ -130,6 +131,7 @@ fun Application.configureDependencies( ) } singleOf(::AnnouncementService) + singleOf(::SignatureService) singleOf(::PatchesService) singleOf(::ApiService) } diff --git a/src/main/kotlin/app/revanced/api/configuration/Logging.kt b/src/main/kotlin/app/revanced/api/configuration/Logging.kt new file mode 100644 index 00000000..48b8eb7d --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Logging.kt @@ -0,0 +1,16 @@ +package app.revanced.api.configuration + +import io.ktor.server.application.* +import io.ktor.server.plugins.callloging.* +import io.ktor.server.request.* + +internal fun Application.configureLogging() { + install(CallLogging) { + format { call -> + val status = call.response.status() + val httpMethod = call.request.httpMethod.value + val uri = call.request.uri + "$status $httpMethod $uri" + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt index b48ee81a..1ed0469a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -4,7 +4,7 @@ import io.ktor.client.* import kotlinx.datetime.LocalDateTime /** - * The backend of the application used to get data for the API. + * The backend of the API used to get data. * * @param client The HTTP client to use for requests. */ @@ -97,12 +97,18 @@ abstract class BackendRepository internal constructor( val createdAt: LocalDateTime, val assets: Set, ) { + companion object { + fun Set.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) } + } + /** * An asset of a release. * + * @property name The name of the asset. * @property downloadUrl The URL to download the asset. */ class BackendAsset( + val name: String, val downloadUrl: String, ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index a7a37023..eaf70be6 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -1,18 +1,78 @@ package app.revanced.api.configuration.repository +import app.revanced.api.configuration.services.PatchesService +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.io.File +/** + * The repository storing the configuration for the API. + * + * @property organization The API backends organization name where the repositories for the patches and integrations are. + * @property patches The source of the patches. + * @property integrations The source of the integrations. + * @property contributorsRepositoryNames The names of the repositories to get contributors from. + * @property apiVersion The version to use for the API. + * @property host The host of the API to configure CORS. + */ @Serializable internal class ConfigurationRepository( val organization: String, - @SerialName("patches-repository") - val patchesRepository: String, - @SerialName("integrations-repositories") - val integrationsRepositoryNames: Set, + val patches: AssetConfiguration, + val integrations: AssetConfiguration, @SerialName("contributors-repositories") val contributorsRepositoryNames: Set, @SerialName("api-version") val apiVersion: Int = 1, val host: String, -) +) { + /** + * An asset configuration. + * + * [PatchesService] uses [BackendRepository] to get assets from its releases. + * A release contains multiple assets. + * + * This configuration is used in [ConfigurationRepository] + * to determine which release assets from repositories to get and to verify them. + * + * @property repository The repository in which releases are made to get an asset. + * @property assetRegex The regex matching the asset name. + * @property signatureAssetRegex The regex matching the signature asset name to verify the asset. + * @property publicKeyFile The public key file to verify the signature of the asset. + */ + @Serializable + internal class AssetConfiguration( + val repository: String, + @Serializable(with = RegexSerializer::class) + @SerialName("asset-regex") + val assetRegex: Regex, + @Serializable(with = RegexSerializer::class) + @SerialName("signature-asset-regex") + val signatureAssetRegex: Regex, + @Serializable(with = FileSerializer::class) + @SerialName("public-key-file") + val publicKeyFile: File, + ) +} + +private object RegexSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Regex", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Regex) = encoder.encodeString(value.pattern) + + override fun deserialize(decoder: Decoder) = Regex(decoder.decodeString()) +} + +private object FileSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("File", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: File) = encoder.encodeString(value.path) + + override fun deserialize(decoder: Decoder) = File(decoder.decodeString()) +} diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt index 8084fb0d..2664afea 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt @@ -35,7 +35,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { releaseNote = release.body, createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), assets = release.assets.map { - BackendAsset(downloadUrl = it.browserDownloadUrl) + BackendAsset( + name = it.name, + downloadUrl = it.browserDownloadUrl, + ) }.toSet(), ) } @@ -156,6 +159,7 @@ class GitHubOrganization { ) { @Serializable class GitHubAsset( + val name: String, val browserDownloadUrl: String, ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index ab11b324..8419e6a5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -1,6 +1,8 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNotarizedRoute +import app.revanced.api.configuration.schema.APIAssetPublicKeys import app.revanced.api.configuration.schema.APIRelease import app.revanced.api.configuration.schema.APIReleaseVersion import app.revanced.api.configuration.services.PatchesService @@ -10,6 +12,7 @@ import io.ktor.server.application.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* +import kotlin.time.Duration.Companion.days import org.koin.ktor.ext.get as koinGet internal fun Route.patchesRoute() = route("patches") { @@ -42,6 +45,18 @@ internal fun Route.patchesRoute() = route("patches") { } } } + + rateLimit(RateLimitName("strong")) { + route("keys") { + installCache(356.days) + + installPatchesPublicKeyRouteDocumentation() + + get { + call.respond(patchesService.publicKeys()) + } + } + } } fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute { @@ -88,3 +103,18 @@ fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute { } } } + +fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute { + tags = setOf("Patches") + + get = GetInfo.builder { + description("Get the public keys for verifying patches and integrations assets") + summary("Get patches and integrations public keys") + response { + description("The public keys") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 1dad95fa..1230ce12 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -1,14 +1,13 @@ package app.revanced.api.configuration.schema import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable class APIRelease( val version: String, val createdAt: LocalDateTime, - val changelog: String, + val description: String, val assets: Set, ) @@ -49,23 +48,15 @@ class APIContributable( @Serializable class APIAsset( val downloadUrl: String, -) { - val type = when { - downloadUrl.endsWith(".jar") -> Type.PATCHES - downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS - else -> Type.UNKNOWN - } - - enum class Type { - @SerialName("patches") - PATCHES, - - @SerialName("integrations") - INTEGRATIONS, - - @SerialName("unknown") - UNKNOWN, - } + val signatureDownloadUrl: String, + // TODO: Remove this eventually when integrations are merged into patches. + val type: APIAssetType, +) + +@Serializable +enum class APIAssetType { + PATCHES, + INTEGRATION, } @Serializable @@ -113,3 +104,9 @@ class APIRateLimit( val remaining: Int, val reset: LocalDateTime, ) + +@Serializable +class APIAssetPublicKeys( + val patchesPublicKey: String, + val integrationsPublicKey: String, +) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index 591e4fe7..abc107de 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -1,87 +1,119 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.BackendRepository +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.schema.APIAsset -import app.revanced.api.configuration.schema.APIRelease -import app.revanced.api.configuration.schema.APIReleaseVersion +import app.revanced.api.configuration.schema.* import app.revanced.library.PatchUtils import app.revanced.patcher.PatchBundleLoader import com.github.benmanes.caffeine.cache.Caffeine -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.withContext +import io.ktor.util.* import java.io.ByteArrayOutputStream import java.net.URL internal class PatchesService( + private val signatureService: SignatureService, private val backendRepository: BackendRepository, private val configurationRepository: ConfigurationRepository, ) { - private val patchesListCache = Caffeine - .newBuilder() - .maximumSize(1) - .build() - suspend fun latestRelease(): APIRelease { val patchesRelease = backendRepository.release( configurationRepository.organization, - configurationRepository.patchesRepository, + configurationRepository.patches.repository, ) - val integrationsReleases = withContext(Dispatchers.Default) { - configurationRepository.integrationsRepositoryNames.map { - async { backendRepository.release(configurationRepository.organization, it) } - } - }.awaitAll() - val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) - .map { APIAsset(it.downloadUrl) } - .filter { it.type != APIAsset.Type.UNKNOWN } - .toSet() + val integrationsRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.integrations.repository, + ) + + fun ConfigurationRepository.AssetConfiguration.asset( + release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease, + assetType: APIAssetType, + ) = APIAsset( + release.assets.first(assetRegex).downloadUrl, + release.assets.first(signatureAssetRegex).downloadUrl, + assetType, + ) + + val patchesAsset = configurationRepository.patches.asset( + patchesRelease, + APIAssetType.PATCHES, + ) + val integrationsAsset = configurationRepository.integrations.asset( + integrationsRelease, + APIAssetType.INTEGRATION, + ) return APIRelease( patchesRelease.tag, patchesRelease.createdAt, patchesRelease.releaseNote, - assets, + setOf(patchesAsset, integrationsAsset), ) } suspend fun latestVersion(): APIReleaseVersion { val patchesRelease = backendRepository.release( configurationRepository.organization, - configurationRepository.patchesRepository, + configurationRepository.patches.repository, ) return APIReleaseVersion(patchesRelease.tag) } + private val patchesListCache = Caffeine + .newBuilder() + .maximumSize(1) + .build() + suspend fun list(): ByteArray { val patchesRelease = backendRepository.release( configurationRepository.organization, - configurationRepository.patchesRepository, + configurationRepository.patches.repository, ) - return patchesListCache.getIfPresent(patchesRelease.tag) ?: run { - val downloadUrl = patchesRelease.assets - .map { APIAsset(it.downloadUrl) } - .find { it.type == APIAsset.Type.PATCHES } - ?.downloadUrl + return patchesListCache.get(patchesRelease.tag) { + val patchesDownloadUrl = patchesRelease.assets + .first(configurationRepository.patches.assetRegex).downloadUrl + + val signatureDownloadUrl = patchesRelease.assets + .first(configurationRepository.patches.signatureAssetRegex).downloadUrl - val patches = kotlin.io.path.createTempFile().toFile().apply { - outputStream().use { URL(downloadUrl).openStream().copyTo(it) } - }.let { file -> - PatchBundleLoader.Jar(file).also { file.delete() } + val patchesFile = kotlin.io.path.createTempFile().toFile().apply { + outputStream().use { URL(patchesDownloadUrl).openStream().copyTo(it) } } + val patches = if ( + signatureService.verify( + patchesFile, + signatureDownloadUrl, + configurationRepository.patches.publicKeyFile, + ) + ) { + PatchBundleLoader.Jar(patchesFile) + } else { + // Use an empty set of patches if the signature is invalid. + emptySet() + } + + patchesFile.delete() + ByteArrayOutputStream().use { stream -> PatchUtils.Json.serialize(patches, outputStream = stream) stream.toByteArray() - }.also { - patchesListCache.put(patchesRelease.tag, it) } } } + + fun publicKeys(): APIAssetPublicKeys { + fun publicKeyBase64(getAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.AssetConfiguration) = + configurationRepository.getAssetConfiguration().publicKeyFile.readBytes().encodeBase64() + + return APIAssetPublicKeys( + publicKeyBase64 { patches }, + publicKeyBase64 { integrations }, + ) + } } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt b/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt new file mode 100644 index 00000000..89ea40e7 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt @@ -0,0 +1,72 @@ +package app.revanced.api.configuration.services + +import com.github.benmanes.caffeine.cache.Caffeine +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider +import java.io.File +import java.io.InputStream +import java.net.URL +import java.security.MessageDigest +import java.security.Security + +internal class SignatureService { + private val signatureCache = Caffeine + .newBuilder() + .maximumSize(2) // Assuming this is enough for patches and integrations. + .build() // Hash -> Verified. + + fun verify( + file: File, + signatureDownloadUrl: String, + publicKeyFile: File, + ): Boolean { + val fileBytes = file.readBytes() + + return signatureCache.get(MessageDigest.getInstance("SHA-256").digest(fileBytes)) { + verify( + fileBytes = fileBytes, + signatureInputStream = URL(signatureDownloadUrl).openStream(), + publicKeyInputStream = publicKeyFile.inputStream(), + ) + } + } + + private fun verify( + fileBytes: ByteArray, + signatureInputStream: InputStream, + publicKeyInputStream: InputStream, + ) = getSignature(signatureInputStream).apply { + init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyInputStream)) + update(fileBytes) + }.verify() + + private fun getPublicKey(publicKeyInputStream: InputStream): PGPPublicKey { + val decoderStream = PGPUtil.getDecoderStream(publicKeyInputStream) + + PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator()).forEach { keyRing -> + keyRing.publicKeys.forEach { publicKey -> + if (publicKey.isEncryptionKey) { + return publicKey + } + } + } + + throw IllegalArgumentException("Can't find encryption key in key ring.") + } + + private fun getSignature(inputStream: InputStream): PGPSignature { + val decoderStream = PGPUtil.getDecoderStream(inputStream) + val pgpObjectFactory = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator()) + val signatureList = pgpObjectFactory.nextObject() as PGPSignatureList + + return signatureList.first() + } + + private companion object { + init { + Security.addProvider(BouncyCastleProvider()) + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index de52ac6e..6f0c78a3 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,8 +4,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + - From d92c1cf3738efbf87691a29c9dabc34d7a7446b1 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 29 Jun 2024 04:14:02 +0200 Subject: [PATCH 63/81] Add swagger and redoc routes --- .../kotlin/app/revanced/api/configuration/Routing.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index f20dc4c4..2ac2109a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -5,22 +5,27 @@ import app.revanced.api.configuration.routes.announcementsRoute import app.revanced.api.configuration.routes.oldApiRoute import app.revanced.api.configuration.routes.patchesRoute import app.revanced.api.configuration.routes.rootRoute +import io.bkbn.kompendium.core.routes.redoc +import io.bkbn.kompendium.core.routes.swagger import io.ktor.server.application.* import io.ktor.server.routing.* -import org.koin.ktor.ext.get import kotlin.time.Duration.Companion.minutes +import org.koin.ktor.ext.get as koinGet internal fun Application.configureRouting() = routing { - val configuration = get() + val configuration = koinGet() installCache(5.minutes) route("/v${configuration.apiVersion}") { - rootRoute() patchesRoute() announcementsRoute() + rootRoute() } + swagger(pageTitle = "ReVanced API", path = "/") + redoc(pageTitle = "ReVanced API", path = "/redoc") + // TODO: Remove, once migration period from v2 API is over (In 1-2 years). oldApiRoute() } From ba936ad134b5d11c879615b17af1c11ba0e23ef5 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 29 Jun 2024 14:41:18 +0200 Subject: [PATCH 64/81] ci: Add deployment steps --- .github/workflows/release.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a4601fc..ed8fceaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,3 +84,17 @@ jobs: build-args: | GITHUB_ACTOR=${{ github.actor }} GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + + - name: Set Portainer stack webhook URL based on branch + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + PORTAINER_WEBHOOK_URL=${{ secrets.PORTAINER_WEBHOOK_MAIN_URL }} + else + PORTAINER_WEBHOOK_URL=${{ secrets.PORTAINER_WEBHOOK_DEV_URL }} + fi + echo "PORTAINER_WEBHOOK_URL=$PORTAINER_WEBHOOK_URL" >> $GITHUB_ENV + + - name: Trigger Portainer stack update + uses: newarifrh/portainer-service-webhook@v1 + with: + webhook_url: ${{ env.PORTAINER_WEBHOOK_URL }} From 7c20e0fe9951817e9f2f5496acbe11b6b1a31d5e Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 29 Jun 2024 15:51:09 +0200 Subject: [PATCH 65/81] build: Update Kompendium --- gradle/libs.versions.toml | 2 +- .../app/revanced/api/configuration/OpenAPI.kt | 59 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cf646231..101125f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kompendium-core = "latest.release" +kompendium-core = "3.14.4" kotlin = "2.0.0" logback = "1.4.14" exposed = "0.41.1" diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index 1dd1fa74..50ee19a9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -16,40 +16,37 @@ import java.net.URI internal fun Application.configureOpenAPI() { install(NotarizedApplication()) { - spec = { - OpenApiSpec( - info = Info( - title = "ReVanced API", - version = applicationVersion, - description = "API server for ReVanced.", - contact = Contact( - name = "ReVanced", - url = URI("https://revanced.app"), - email = "contact@revanced.app", - ), - license = License( - name = "AGPLv3", - url = URI("https://github.com/ReVanced/revanced-api/blob/main/LICENSE"), - ), + spec = OpenApiSpec( + info = Info( + title = "ReVanced API", + version = applicationVersion, + description = "API server for ReVanced.", + contact = Contact( + name = "ReVanced", + url = URI("https://revanced.app"), + email = "contact@revanced.app", ), - components = Components( - securitySchemes = mutableMapOf( - "bearer" to BearerAuth(), - "basic" to BasicAuth(), - ), + license = License( + name = "AGPLv3", + url = URI("https://github.com/ReVanced/revanced-api/blob/main/LICENSE"), ), + ), + components = Components( + securitySchemes = mutableMapOf( + "bearer" to BearerAuth(), + "basic" to BasicAuth(), + ), + ), + ).apply { + servers += Server( + url = URI("https://api.revanced.app"), + description = "ReVanced API server", + ) - ).apply { - servers += Server( - url = URI("https://api.revanced.app"), - description = "ReVanced API server", - ) - - servers += Server( - url = URI("http://localhost:8888"), - description = "Local ReVanced API server", - ) - } + servers += Server( + url = URI("http://localhost:8888"), + description = "Local ReVanced API server", + ) } schemaConfigurator = KotlinXSchemaConfigurator() From 744aafa9f081553cf1b5b2d887920350e1660dab Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 29 Jun 2024 15:51:30 +0200 Subject: [PATCH 66/81] chore: Fix Docker compose port mapping --- docker-compose.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index c8c032aa..a82e58e8 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -11,5 +11,5 @@ services: environment: - COMMAND=start ports: - - localhost:8888:8888 + - 8888:8888 restart: unless-stopped From e9d1c8fae0bc46e761056197658c4bb045784104 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 29 Jun 2024 23:40:32 +0200 Subject: [PATCH 67/81] fix: Encode defaults to fix OpenAPI spec --- src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt | 5 ----- .../kotlin/app/revanced/api/configuration/Serialization.kt | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index 50ee19a9..45316ef5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -42,11 +42,6 @@ internal fun Application.configureOpenAPI() { url = URI("https://api.revanced.app"), description = "ReVanced API server", ) - - servers += Server( - url = URI("http://localhost:8888"), - description = "Local ReVanced API server", - ) } schemaConfigurator = KotlinXSchemaConfigurator() diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt index ceb6182c..1c4b939c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -16,6 +16,7 @@ fun Application.configureSerialization() { serializersModule = KompendiumSerializersModule.module namingStrategy = JsonNamingStrategy.SnakeCase explicitNulls = false + encodeDefaults = true }, ) } From 7f8f1ff589431f3825f7075f72c51f5c5dd42756 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 1 Jul 2024 00:35:28 +0200 Subject: [PATCH 68/81] Use IO context for IO operations --- .../repository/AnnouncementRepository.kt | 35 +++++----- .../services/AnnouncementService.kt | 22 +++---- .../configuration/services/PatchesService.kt | 64 ++++++++++--------- 3 files changed, 65 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index eb8475e1..9835454e 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -4,6 +4,8 @@ import app.revanced.api.configuration.repository.AnnouncementRepository.Attachme import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncementId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.datetime.* import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass @@ -11,16 +13,18 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.kotlin.datetime.datetime -import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction internal class AnnouncementRepository(private val database: Database) { init { - transaction { - SchemaUtils.create(AnnouncementTable, AttachmentTable) + runBlocking { + transaction { + SchemaUtils.create(AnnouncementTable, AttachmentTable) + } } } - fun all() = transaction { + suspend fun all() = transaction { buildSet { AnnouncementEntity.all().forEach { announcement -> add(announcement.toApi()) @@ -28,7 +32,7 @@ internal class AnnouncementRepository(private val database: Database) { } } - fun all(channel: String) = transaction { + suspend fun all(channel: String) = transaction { buildSet { AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement -> add(announcement.toApi()) @@ -36,7 +40,7 @@ internal class AnnouncementRepository(private val database: Database) { } } - fun delete(id: Int) = transaction { + suspend fun delete(id: Int) = transaction { val announcement = AnnouncementEntity.findById(id) ?: return@transaction announcement.delete() @@ -44,27 +48,27 @@ internal class AnnouncementRepository(private val database: Database) { // TODO: These are inefficient, but I'm not sure how to make them more efficient. - fun latest() = transaction { + suspend fun latest() = transaction { AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() } - fun latest(channel: String) = transaction { + suspend fun latest(channel: String) = transaction { AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi() } - fun latestId() = transaction { + suspend fun latestId() = transaction { AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { APIResponseAnnouncementId(it) } } - fun latestId(channel: String) = transaction { + suspend fun latestId(channel: String) = transaction { AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { APIResponseAnnouncementId(it) } } - fun archive( + suspend fun archive( id: Int, archivedAt: LocalDateTime?, ) = transaction { @@ -73,13 +77,13 @@ internal class AnnouncementRepository(private val database: Database) { } } - fun unarchive(id: Int) = transaction { + suspend fun unarchive(id: Int) = transaction { AnnouncementEntity.findById(id)?.apply { archivedAt = null } } - fun new(new: APIAnnouncement) = transaction { + suspend fun new(new: APIAnnouncement) = transaction { AnnouncementEntity.new announcement@{ author = new.author title = new.title @@ -98,7 +102,7 @@ internal class AnnouncementRepository(private val database: Database) { } } - fun update(id: Int, new: APIAnnouncement) = transaction { + suspend fun update(id: Int, new: APIAnnouncement) = transaction { AnnouncementEntity.findById(id)?.apply { author = new.author title = new.title @@ -117,7 +121,8 @@ internal class AnnouncementRepository(private val database: Database) { } } - private fun transaction(block: Transaction.() -> T) = transaction(database, block) + private suspend fun transaction(statement: Transaction.() -> T) = + newSuspendedTransaction(Dispatchers.IO, database, statement = statement) private object AnnouncementTable : IntIdTable() { val author = varchar("author", 32).nullable() diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 7a0b4603..7befd578 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -8,28 +8,28 @@ import kotlinx.datetime.LocalDateTime internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) - fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() + suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) + suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() - fun latest(channel: String) = announcementRepository.latest(channel) - fun latest() = announcementRepository.latest() + suspend fun latest(channel: String) = announcementRepository.latest(channel) + suspend fun latest() = announcementRepository.latest() - fun all(channel: String) = announcementRepository.all(channel) - fun all() = announcementRepository.all() + suspend fun all(channel: String) = announcementRepository.all(channel) + suspend fun all() = announcementRepository.all() - fun new(new: APIAnnouncement) { + suspend fun new(new: APIAnnouncement) { announcementRepository.new(new) } - fun archive(id: Int, archivedAt: LocalDateTime?) { + suspend fun archive(id: Int, archivedAt: LocalDateTime?) { announcementRepository.archive(id, archivedAt) } - fun unarchive(id: Int) { + suspend fun unarchive(id: Int) { announcementRepository.unarchive(id) } - fun update(id: Int, new: APIAnnouncement) { + suspend fun update(id: Int, new: APIAnnouncement) { announcementRepository.update(id, new) } - fun delete(id: Int) { + suspend fun delete(id: Int) { announcementRepository.delete(id) } } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index abc107de..2024b679 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -8,6 +8,8 @@ import app.revanced.library.PatchUtils import app.revanced.patcher.PatchBundleLoader import com.github.benmanes.caffeine.cache.Caffeine import io.ktor.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.net.URL @@ -73,36 +75,38 @@ internal class PatchesService( configurationRepository.patches.repository, ) - return patchesListCache.get(patchesRelease.tag) { - val patchesDownloadUrl = patchesRelease.assets - .first(configurationRepository.patches.assetRegex).downloadUrl - - val signatureDownloadUrl = patchesRelease.assets - .first(configurationRepository.patches.signatureAssetRegex).downloadUrl - - val patchesFile = kotlin.io.path.createTempFile().toFile().apply { - outputStream().use { URL(patchesDownloadUrl).openStream().copyTo(it) } - } - - val patches = if ( - signatureService.verify( - patchesFile, - signatureDownloadUrl, - configurationRepository.patches.publicKeyFile, - ) - ) { - PatchBundleLoader.Jar(patchesFile) - } else { - // Use an empty set of patches if the signature is invalid. - emptySet() - } - - patchesFile.delete() - - ByteArrayOutputStream().use { stream -> - PatchUtils.Json.serialize(patches, outputStream = stream) - - stream.toByteArray() + return withContext(Dispatchers.IO) { + patchesListCache.get(patchesRelease.tag) { + val patchesDownloadUrl = patchesRelease.assets + .first(configurationRepository.patches.assetRegex).downloadUrl + + val signatureDownloadUrl = patchesRelease.assets + .first(configurationRepository.patches.signatureAssetRegex).downloadUrl + + val patchesFile = kotlin.io.path.createTempFile().toFile().apply { + outputStream().use { URL(patchesDownloadUrl).openStream().copyTo(it) } + } + + val patches = if ( + signatureService.verify( + patchesFile, + signatureDownloadUrl, + configurationRepository.patches.publicKeyFile, + ) + ) { + PatchBundleLoader.Jar(patchesFile) + } else { + // Use an empty set of patches if the signature is invalid. + emptySet() + } + + patchesFile.delete() + + ByteArrayOutputStream().use { stream -> + PatchUtils.Json.serialize(patches, outputStream = stream) + + stream.toByteArray() + } } } } From 1ec6999ddbc1f50410ad7ca8447b8cefde51722d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 1 Jul 2024 00:44:14 +0200 Subject: [PATCH 69/81] Bump packages and add files to .gitignore --- .gitignore | 4 +++- gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 083ef1ca..ef1e3e81 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ out/ .env persistence/ configuration.toml -docker-compose.yml \ No newline at end of file +docker-compose.yml +patches-public-key.asc +integrations-public-key.asc \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 101125f7..df974ce3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ dotenv = "6.4.1" ktor = "2.3.7" ktoml = "0.5.1" picocli = "4.7.6" -datetime = "0.5.0" +datetime = "0.6.0" revanced-patcher = "19.3.1" revanced-library = "2.3.0" caffeine = "3.1.8" From df325317702cea0d1317255e03c7b27e44e97d6b Mon Sep 17 00:00:00 2001 From: Ushie Date: Sun, 7 Jul 2024 21:28:04 +0300 Subject: [PATCH 70/81] build: Publish Docker images with semantic-release (#179) --- .github/workflows/release.yml | 44 ++++++++--------------------------- .releaserc | 22 ++++++++++++++---- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed8fceaa..531a377c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: "lts/*" - cache: 'npm' + cache: "npm" - name: Install dependencies run: npm install @@ -52,39 +52,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} run: npm exec semantic-release - - name: Setup QEMU - uses: docker/setup-qemu-action@v3 - - - name: Extract metadata for the Docker image - id: metadata - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ env.IMAGE_NAME }} - flavor: | - latest=${{ startsWith(github.ref, 'refs/heads/main') }} - suffix=-${{ github.sha }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push Docker image - id: build - uses: docker/build-push-action@v5 - with: - platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max,ignore-error=true - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - build-args: | - GITHUB_ACTOR=${{ github.actor }} - GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} - - name: Set Portainer stack webhook URL based on branch run: | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then @@ -98,3 +65,12 @@ jobs: uses: newarifrh/portainer-service-webhook@v1 with: webhook_url: ${{ env.PORTAINER_WEBHOOK_URL }} + + - name: Purge outdated images + uses: snok/container-retention-policy@v3 + with: + account: ${{ github.actor }} + token: ${{ secrets.GITHUB_TOKEN }} + image-names: "revanced-api" + keep-n-most-recent: 5 + cut-off: 3M diff --git a/.releaserc b/.releaserc index 193b3fbf..756d3a4f 100644 --- a/.releaserc +++ b/.releaserc @@ -23,7 +23,7 @@ "assets": [ "README.md", "CHANGELOG.md", - "gradle.properties", + "gradle.properties" ] } ], @@ -35,15 +35,27 @@ "path": "build/libs/*" } ], - successComment: false + "successComment": false + } + ], + [ + "@codedependant/semantic-release-docker", + { + "dockerImage": "revanced-api", + "dockerRegistry": "ghcr.io", + "dockerProject": "revanced", + "dockerPlatform": [ + "linux/amd64", + "linux/arm64/v8" + ] } ], [ "@saithodev/semantic-release-backmerge", { - backmergeBranches: [{"from": "main", "to": "dev"}], - clearWorkspace: true + "backmergeBranches": [{"from": "main", "to": "dev"}], + "clearWorkspace": true } ] ] -} +} \ No newline at end of file From d9c6e521a675d7a0f004fb6ecff39c4ffbd119ab Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 1 Jul 2024 00:47:57 +0200 Subject: [PATCH 71/81] Rename field --- .../app/revanced/api/configuration/schema/APISchema.kt | 4 ++-- .../revanced/api/configuration/services/PatchesService.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 1230ce12..8accc5a0 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -50,11 +50,11 @@ class APIAsset( val downloadUrl: String, val signatureDownloadUrl: String, // TODO: Remove this eventually when integrations are merged into patches. - val type: APIAssetType, + val name: APIAssetName, ) @Serializable -enum class APIAssetType { +enum class APIAssetName { PATCHES, INTEGRATION, } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index 2024b679..de234426 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -31,20 +31,20 @@ internal class PatchesService( fun ConfigurationRepository.AssetConfiguration.asset( release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease, - assetType: APIAssetType, + assetName: APIAssetName, ) = APIAsset( release.assets.first(assetRegex).downloadUrl, release.assets.first(signatureAssetRegex).downloadUrl, - assetType, + assetName, ) val patchesAsset = configurationRepository.patches.asset( patchesRelease, - APIAssetType.PATCHES, + APIAssetName.PATCHES, ) val integrationsAsset = configurationRepository.integrations.asset( integrationsRelease, - APIAssetType.INTEGRATION, + APIAssetName.INTEGRATION, ) return APIRelease( From d4b228faa758af3aca7156d7a00656acc526a52b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 1 Jul 2024 01:09:05 +0200 Subject: [PATCH 72/81] Use correct server url in OpenAPI spec --- configuration.example.toml | 3 ++- configuration.toml | 3 ++- .../app/revanced/api/configuration/HTTP.kt | 5 ++++- .../app/revanced/api/configuration/OpenAPI.kt | 6 +++++- .../repository/ConfigurationRepository.kt | 19 +++++++++++++++++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/configuration.example.toml b/configuration.example.toml index 1530f04f..47ff97e5 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -10,4 +10,5 @@ contributors-repositories = [ "revanced-manager", ] api-version = 1 -host = "*.revanced.app" +cors = { host = "*.revanced.app", sub-domains = [] } +endpoint = "https://api.revanced.app" \ No newline at end of file diff --git a/configuration.toml b/configuration.toml index 28155952..37da2608 100644 --- a/configuration.toml +++ b/configuration.toml @@ -10,4 +10,5 @@ contributors-repositories = [ "revanced-manager", ] api-version = 1 -host = "*.revanced.app" \ No newline at end of file +cors = { host = "*.127.0.0.1:8888", sub-domains = [] } +endpoint = "http://127.0.0.1:8888/" \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt index 09811a12..aa970c69 100644 --- a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -12,7 +12,10 @@ fun Application.configureHTTP() { val configurationRepository = get() install(CORS) { - allowHost(host = configurationRepository.host) + allowHost( + host = configurationRepository.cors.host, + subDomains = configurationRepository.cors.subDomains, + ) } install(RateLimit) { diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index 45316ef5..e1c637de 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -1,6 +1,7 @@ package app.revanced.api.configuration import app.revanced.api.command.applicationVersion +import app.revanced.api.configuration.repository.ConfigurationRepository import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator import io.bkbn.kompendium.oas.OpenApiSpec @@ -12,9 +13,12 @@ import io.bkbn.kompendium.oas.security.BasicAuth import io.bkbn.kompendium.oas.security.BearerAuth import io.bkbn.kompendium.oas.server.Server import io.ktor.server.application.* +import org.koin.ktor.ext.get import java.net.URI internal fun Application.configureOpenAPI() { + val configurationRepository = get() + install(NotarizedApplication()) { spec = OpenApiSpec( info = Info( @@ -39,7 +43,7 @@ internal fun Application.configureOpenAPI() { ), ).apply { servers += Server( - url = URI("https://api.revanced.app"), + url = URI(configurationRepository.endpoint), description = "ReVanced API server", ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index eaf70be6..aed455aa 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -19,7 +19,8 @@ import java.io.File * @property integrations The source of the integrations. * @property contributorsRepositoryNames The names of the repositories to get contributors from. * @property apiVersion The version to use for the API. - * @property host The host of the API to configure CORS. + * @property cors The CORS configuration. + * @property endpoint The endpoint of the API. */ @Serializable internal class ConfigurationRepository( @@ -30,7 +31,8 @@ internal class ConfigurationRepository( val contributorsRepositoryNames: Set, @SerialName("api-version") val apiVersion: Int = 1, - val host: String, + val cors: Cors, + val endpoint: String, ) { /** * An asset configuration. @@ -59,6 +61,19 @@ internal class ConfigurationRepository( @SerialName("public-key-file") val publicKeyFile: File, ) + + /** + * The CORS configuration. + * + * @property host The host of the API to configure CORS. + * @property subDomains The subdomains to allow for CORS. + */ + @Serializable + internal class Cors( + val host: String, + @SerialName("sub-domains") + val subDomains: List, + ) } private object RegexSerializer : KSerializer { From 01780188b9abba21dd66375895515c4710790435 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 00:39:35 +0200 Subject: [PATCH 73/81] build: Update dependencies --- build.gradle.kts | 4 ++-- gradle/libs.versions.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index de068fe8..45481207 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,8 +13,8 @@ tasks { expand("projectVersion" to project.version) } - // Needed by gradle-semantic-release-plugin. - // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 + // Used by gradle-semantic-release-plugin. + // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435. publish { dependsOn(shadowJar) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df974ce3..6730386a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] kompendium-core = "3.14.4" kotlin = "2.0.0" -logback = "1.4.14" -exposed = "0.41.1" +logback = "1.5.6" +exposed = "0.52.0" h2 = "2.2.224" koin = "3.5.3" dotenv = "6.4.1" ktor = "2.3.7" -ktoml = "0.5.1" +ktoml = "0.5.2" picocli = "4.7.6" datetime = "0.6.0" revanced-patcher = "19.3.1" From a7d1892343094ddb2762ca346759771e6a274081 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 00:41:05 +0200 Subject: [PATCH 74/81] perf: Make async db transactions and use List instead of Set --- .../api/configuration/Dependencies.kt | 25 +-- .../repository/AnnouncementRepository.kt | 152 ++++++++---------- .../repository/BackendRepository.kt | 18 ++- .../repository/GitHubBackendRepository.kt | 23 +-- .../api/configuration/schema/APISchema.kt | 12 +- .../services/AnnouncementService.kt | 21 ++- .../api/configuration/services/ApiService.kt | 6 +- .../configuration/services/PatchesService.kt | 2 +- 8 files changed, 134 insertions(+), 125 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 24ebb542..5256dd45 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.koin.core.module.dsl.singleOf import org.koin.core.parameter.parameterArrayOf import org.koin.dsl.module @@ -41,6 +42,7 @@ fun Application.configureDependencies( single { Dotenv.configure().load() } + factory { params -> val defaultRequestUri: String = params.get() val configBlock = params.getOrNull<(HttpClientConfig.() -> Unit)>() ?: {} @@ -54,17 +56,6 @@ fun Application.configureDependencies( } val repositoryModule = module { - single { - val dotenv = get() - - Database.connect( - url = dotenv["DB_URL"], - user = dotenv["DB_USER"], - password = dotenv["DB_PASSWORD"], - driver = "org.h2.Driver", - ) - } - single { GitHubBackendRepository( get { @@ -106,7 +97,17 @@ fun Application.configureDependencies( Toml.decodeFromStream(configFile.inputStream()) } - singleOf(::AnnouncementRepository) + single { + val dotenv = get() + + TransactionManager.defaultDatabase = Database.connect( + url = dotenv["DB_URL"], + user = dotenv["DB_USER"], + password = dotenv["DB_PASSWORD"], + ) + + AnnouncementRepository() + } } val serviceModule = module { diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index 9835454e..95c4daf0 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,47 +1,41 @@ package app.revanced.api.configuration.repository -import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement import app.revanced.api.configuration.schema.APIAnnouncement -import app.revanced.api.configuration.schema.APIResponseAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import kotlinx.datetime.* import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.dao.load import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync -internal class AnnouncementRepository(private val database: Database) { +internal class AnnouncementRepository { init { runBlocking { transaction { - SchemaUtils.create(AnnouncementTable, AttachmentTable) + SchemaUtils.create(Announcements, Attachments) } } } suspend fun all() = transaction { - buildSet { - AnnouncementEntity.all().forEach { announcement -> - add(announcement.toApi()) - } - } + Announcement.all() } suspend fun all(channel: String) = transaction { - buildSet { - AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement -> - add(announcement.toApi()) - } - } + Announcement.find { Announcements.channel eq channel } } suspend fun delete(id: Int) = transaction { - val announcement = AnnouncementEntity.findById(id) ?: return@transaction + val announcement = Announcement.findById(id) ?: return@transaction announcement.delete() } @@ -49,21 +43,21 @@ internal class AnnouncementRepository(private val database: Database) { // TODO: These are inefficient, but I'm not sure how to make them more efficient. suspend fun latest() = transaction { - AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() + Announcement.all().maxByOrNull { it.id }?.load(Announcement::attachments) } suspend fun latest(channel: String) = transaction { - AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi() + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.load(Announcement::attachments) } suspend fun latestId() = transaction { - AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { + Announcement.all().maxByOrNull { it.id }?.id?.value?.let { APIResponseAnnouncementId(it) } } suspend fun latestId(channel: String) = transaction { - AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.id?.value?.let { APIResponseAnnouncementId(it) } } @@ -72,106 +66,98 @@ internal class AnnouncementRepository(private val database: Database) { id: Int, archivedAt: LocalDateTime?, ) = transaction { - AnnouncementEntity.findById(id)?.apply { - this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() + Announcement.findByIdAndUpdate(id) { + it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() } } suspend fun unarchive(id: Int) = transaction { - AnnouncementEntity.findById(id)?.apply { - archivedAt = null + Announcement.findByIdAndUpdate(id) { + it.archivedAt = null } } suspend fun new(new: APIAnnouncement) = transaction { - AnnouncementEntity.new announcement@{ + Announcement.new { author = new.author title = new.title content = new.content channel = new.channel - createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) archivedAt = new.archivedAt level = new.level }.also { newAnnouncement -> - new.attachmentUrls.map { - AttachmentEntity.new { - url = it - announcement = newAnnouncement + new.attachmentUrls.map { newUrl -> + suspendedTransactionAsync { + Attachment.new { + url = newUrl + announcement = newAnnouncement + } } - } + }.awaitAll() } } suspend fun update(id: Int, new: APIAnnouncement) = transaction { - AnnouncementEntity.findById(id)?.apply { - author = new.author - title = new.title - content = new.content - channel = new.channel - archivedAt = new.archivedAt - level = new.level - - attachments.forEach(AttachmentEntity::delete) - new.attachmentUrls.map { - AttachmentEntity.new { - url = it - announcement = this@apply + Announcement.findByIdAndUpdate(id) { + it.author = new.author + it.title = new.title + it.content = new.content + it.channel = new.channel + it.archivedAt = new.archivedAt + it.level = new.level + }?.also { newAnnouncement -> + newAnnouncement.attachments.map { + suspendedTransactionAsync { + it.delete() } - } + }.awaitAll() + + new.attachmentUrls.map { newUrl -> + suspendedTransactionAsync { + Attachment.new { + url = newUrl + announcement = newAnnouncement + } + } + }.awaitAll() } } - private suspend fun transaction(statement: Transaction.() -> T) = - newSuspendedTransaction(Dispatchers.IO, database, statement = statement) + private suspend fun transaction(statement: suspend Transaction.() -> T) = + newSuspendedTransaction(Dispatchers.IO, statement = statement) - private object AnnouncementTable : IntIdTable() { + private object Announcements : IntIdTable() { val author = varchar("author", 32).nullable() val title = varchar("title", 64) val content = text("content").nullable() val channel = varchar("channel", 16).nullable() - val createdAt = datetime("createdAt") + val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime) val archivedAt = datetime("archivedAt").nullable() val level = integer("level") } - private object AttachmentTable : IntIdTable() { + private object Attachments : IntIdTable() { val url = varchar("url", 256) - val announcement = reference("announcement", AnnouncementTable, onDelete = ReferenceOption.CASCADE) - } - - class AnnouncementEntity(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(AnnouncementTable) - - var author by AnnouncementTable.author - var title by AnnouncementTable.title - var content by AnnouncementTable.content - val attachments by AttachmentEntity referrersOn announcement - var channel by AnnouncementTable.channel - var createdAt by AnnouncementTable.createdAt - var archivedAt by AnnouncementTable.archivedAt - var level by AnnouncementTable.level - - fun toApi() = APIResponseAnnouncement( - id.value, - author, - title, - content, - attachmentUrls = buildSet { - attachments.forEach { - add(it.url) - } - }, - channel, - createdAt, - archivedAt, - level, - ) + val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) + } + + class Announcement(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Announcements) + + var author by Announcements.author + var title by Announcements.title + var content by Announcements.content + val attachments by Attachment referrersOn Attachments.announcement + var channel by Announcements.channel + var createdAt by Announcements.createdAt + var archivedAt by Announcements.archivedAt + var level by Announcements.level } - class AttachmentEntity(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(AttachmentTable) + class Attachment(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Attachments) - var url by AttachmentTable.url - var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement + var url by Attachments.url + var announcement by Announcement referencedOn Attachments.announcement } } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt index 1ed0469a..69429ca3 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -30,7 +30,8 @@ abstract class BackendRepository internal constructor( * @property members The members of the organization. */ class BackendOrganization( - val members: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val members: List, ) { /** * A member of an organization. @@ -55,7 +56,8 @@ abstract class BackendRepository internal constructor( * @property url The URL to the GPG master key. */ class GpgKeys( - val ids: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val ids: List, val url: String, ) } @@ -66,7 +68,8 @@ abstract class BackendRepository internal constructor( * @property contributors The contributors of the repository. */ class BackendRepository( - val contributors: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val contributors: List, ) { /** * A contributor of a repository. @@ -95,10 +98,11 @@ abstract class BackendRepository internal constructor( val tag: String, val releaseNote: String, val createdAt: LocalDateTime, - val assets: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val assets: List, ) { companion object { - fun Set.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) } + fun List.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) } } /** @@ -149,7 +153,7 @@ abstract class BackendRepository internal constructor( * @param repository The name of the repository. * @return The contributors. */ - abstract suspend fun contributors(owner: String, repository: String): Set + abstract suspend fun contributors(owner: String, repository: String): List /** * Get the members of an organization. @@ -157,7 +161,7 @@ abstract class BackendRepository internal constructor( * @param organization The name of the organization. * @return The members. */ - abstract suspend fun members(organization: String): Set + abstract suspend fun members(organization: String): List /** * Get the rate limit of the backend. diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt index 2664afea..a032f4d7 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt @@ -39,15 +39,15 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { name = it.name, downloadUrl = it.browserDownloadUrl, ) - }.toSet(), + }, ) } override suspend fun contributors( owner: String, repository: String, - ): Set { - val contributors: Set = client.get( + ): List { + val contributors: List = client.get( Contributors( owner, repository, @@ -61,12 +61,12 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { url = it.htmlUrl, contributions = it.contributions, ) - }.toSet() + } } - override suspend fun members(organization: String): Set { + override suspend fun members(organization: String): List { // Get the list of members of the organization. - val members: Set = client.get(Organization.Members(organization)).body() + val members: List = client.get(Organization.Members(organization)).body() return coroutineScope { members.map { member -> @@ -78,7 +78,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { }, async { // Get the GPG key of the user. - client.get(User.GpgKeys(member.login)).body>() + client.get(User.GpgKeys(member.login)).body>() }, ) } @@ -87,7 +87,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { val user = responses[0] as GitHubUser @Suppress("UNCHECKED_CAST") - val gpgKeys = responses[1] as Set + val gpgKeys = responses[1] as List BackendMember( name = user.login, @@ -96,11 +96,11 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { bio = user.bio, gpgKeys = BackendMember.GpgKeys( - ids = gpgKeys.map { it.keyId }.toSet(), + ids = gpgKeys.map { it.keyId }, url = "https://api.github.com/users/${user.login}.gpg", ), ) - }.toSet() + } } override suspend fun rateLimit(): BackendRateLimit { @@ -153,7 +153,8 @@ class GitHubOrganization { @Serializable class GitHubRelease( val tagName: String, - val assets: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val assets: List, val createdAt: Instant, val body: String, ) { diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 8accc5a0..888f8118 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -8,7 +8,8 @@ class APIRelease( val version: String, val createdAt: LocalDateTime, val description: String, - val assets: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val assets: List, ) interface APIUser { @@ -42,7 +43,8 @@ class APIContributor( @Serializable class APIContributable( val name: String, - val contributors: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val contributors: List, ) @Serializable @@ -69,7 +71,8 @@ class APIAnnouncement( val author: String? = null, val title: String, val content: String? = null, - val attachmentUrls: Set = emptySet(), + // Using a list instead of a set because set semantics are unnecessary here. + val attachmentUrls: List = emptyList(), val channel: String? = null, val archivedAt: LocalDateTime? = null, val level: Int = 0, @@ -81,7 +84,8 @@ class APIResponseAnnouncement( val author: String? = null, val title: String, val content: String? = null, - val attachmentUrls: Set = emptySet(), + // Using a list instead of a set because set semantics are unnecessary here. + val attachmentUrls: List = emptyList(), val channel: String? = null, val createdAt: LocalDateTime, val archivedAt: LocalDateTime? = null, diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 7befd578..b0142e5d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -2,6 +2,7 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.AnnouncementRepository import app.revanced.api.configuration.schema.APIAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.datetime.LocalDateTime @@ -11,11 +12,11 @@ internal class AnnouncementService( suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() - suspend fun latest(channel: String) = announcementRepository.latest(channel) - suspend fun latest() = announcementRepository.latest() + suspend fun latest(channel: String) = announcementRepository.latest(channel)?.toApi() + suspend fun latest() = announcementRepository.latest()?.toApi() - suspend fun all(channel: String) = announcementRepository.all(channel) - suspend fun all() = announcementRepository.all() + suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() } + suspend fun all() = announcementRepository.all().map { it.toApi() } suspend fun new(new: APIAnnouncement) { announcementRepository.new(new) @@ -32,4 +33,16 @@ internal class AnnouncementService( suspend fun delete(id: Int) { announcementRepository.delete(id) } + + private fun AnnouncementRepository.Announcement.toApi() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachments.map { it.url }, + channel, + createdAt, + archivedAt, + level, + ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index 10b54cf3..7834362f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -19,11 +19,11 @@ internal class ApiService( it, backendRepository.contributors(configurationRepository.organization, it).map { APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet(), + }, ) } } - }.awaitAll().toSet() + }.awaitAll() suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> APIMember( @@ -41,7 +41,7 @@ internal class ApiService( }, ) - }.toSet() + } suspend fun rateLimit() = backendRepository.rateLimit()?.let { APIRateLimit(it.limit, it.remaining, it.reset) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index de234426..31a0670f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -51,7 +51,7 @@ internal class PatchesService( patchesRelease.tag, patchesRelease.createdAt, patchesRelease.releaseNote, - setOf(patchesAsset, integrationsAsset), + listOf(patchesAsset, integrationsAsset), ) } From 1ca9952de81feeae1333872a7741119d6d8e7814 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 01:54:37 +0200 Subject: [PATCH 75/81] perf: Cache latest announcements for constant access time --- .../repository/AnnouncementRepository.kt | 58 +++++++++++-------- .../services/AnnouncementService.kt | 10 ++-- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index 95c4daf0..c0956693 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,7 +1,6 @@ package app.revanced.api.configuration.repository import app.revanced.api.configuration.schema.APIAnnouncement -import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking @@ -10,7 +9,6 @@ import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable -import org.jetbrains.exposed.dao.load import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime import org.jetbrains.exposed.sql.kotlin.datetime.datetime @@ -18,10 +16,26 @@ import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransacti import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync internal class AnnouncementRepository { + // This is better than doing a maxByOrNull { it.id }. + private var latestAnnouncement: Announcement? = null + private val latestAnnouncementByChannel = mutableMapOf() + + private fun updateLatestAnnouncement(new: Announcement) { + if (latestAnnouncement?.id?.value == new.id.value) { + latestAnnouncement = new + latestAnnouncementByChannel[new.channel ?: return] = new + } + } + init { runBlocking { transaction { SchemaUtils.create(Announcements, Attachments) + + // Initialize the latest announcement. + latestAnnouncement = Announcement.all().onEach { + latestAnnouncementByChannel[it.channel ?: return@onEach] = it + }.maxByOrNull { it.id } ?: return@transaction } } } @@ -38,29 +52,27 @@ internal class AnnouncementRepository { val announcement = Announcement.findById(id) ?: return@transaction announcement.delete() - } - // TODO: These are inefficient, but I'm not sure how to make them more efficient. + // In case the latest announcement was deleted, query the new latest announcement again. + if (latestAnnouncement?.id?.value == id) { + latestAnnouncement = Announcement.all().maxByOrNull { it.id } - suspend fun latest() = transaction { - Announcement.all().maxByOrNull { it.id }?.load(Announcement::attachments) + // If no latest announcement was found, remove it from the channel map. + if (latestAnnouncement == null) { + latestAnnouncementByChannel.remove(announcement.channel) + } else { + latestAnnouncementByChannel[latestAnnouncement!!.channel ?: return@transaction] = latestAnnouncement!! + } + } } - suspend fun latest(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.load(Announcement::attachments) - } + fun latest() = latestAnnouncement - suspend fun latestId() = transaction { - Announcement.all().maxByOrNull { it.id }?.id?.value?.let { - APIResponseAnnouncementId(it) - } - } + fun latest(channel: String) = latestAnnouncementByChannel[channel] - suspend fun latestId(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.id?.value?.let { - APIResponseAnnouncementId(it) - } - } + fun latestId() = latest()?.id?.value + + fun latestId(channel: String) = latest(channel)?.id?.value suspend fun archive( id: Int, @@ -68,13 +80,13 @@ internal class AnnouncementRepository { ) = transaction { Announcement.findByIdAndUpdate(id) { it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() - } + }?.also(::updateLatestAnnouncement) } suspend fun unarchive(id: Int) = transaction { Announcement.findByIdAndUpdate(id) { it.archivedAt = null - } + }?.also(::updateLatestAnnouncement) } suspend fun new(new: APIAnnouncement) = transaction { @@ -94,7 +106,7 @@ internal class AnnouncementRepository { } } }.awaitAll() - } + }.also(::updateLatestAnnouncement) } suspend fun update(id: Int, new: APIAnnouncement) = transaction { @@ -120,7 +132,7 @@ internal class AnnouncementRepository { } } }.awaitAll() - } + }?.also(::updateLatestAnnouncement) } private suspend fun transaction(statement: suspend Transaction.() -> T) = diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index b0142e5d..ae4cf7ea 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -9,11 +9,11 @@ import kotlinx.datetime.LocalDateTime internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) - suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() + fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)?.toApi() + fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()?.toApi() - suspend fun latest(channel: String) = announcementRepository.latest(channel)?.toApi() - suspend fun latest() = announcementRepository.latest()?.toApi() + fun latest(channel: String) = announcementRepository.latest(channel)?.toApi() + fun latest() = announcementRepository.latest()?.toApi() suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() } suspend fun all() = announcementRepository.all().map { it.toApi() } @@ -45,4 +45,6 @@ internal class AnnouncementService( archivedAt, level, ) + + private fun Int.toApi() = APIResponseAnnouncementId(this) } From 89a577e91abbfcd2865e770088661eac4aeb4dd7 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 05:16:59 +0200 Subject: [PATCH 76/81] fix: Finish DB Model to API model transformation inside transaction --- .../repository/AnnouncementRepository.kt | 28 +++++++++++++++---- .../services/AnnouncementService.kt | 27 ++++-------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index c0956693..992b06ff 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,6 +1,8 @@ package app.revanced.api.configuration.repository import app.revanced.api.configuration.schema.APIAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncement +import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking @@ -41,11 +43,11 @@ internal class AnnouncementRepository { } suspend fun all() = transaction { - Announcement.all() + Announcement.all().map { it.toApi() } } suspend fun all(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel } + Announcement.find { Announcements.channel eq channel }.map { it.toApi() } } suspend fun delete(id: Int) = transaction { @@ -66,13 +68,13 @@ internal class AnnouncementRepository { } } - fun latest() = latestAnnouncement + fun latest() = latestAnnouncement?.toApi() - fun latest(channel: String) = latestAnnouncementByChannel[channel] + fun latest(channel: String) = latestAnnouncementByChannel[channel]?.toApi() - fun latestId() = latest()?.id?.value + fun latestId() = latest()?.id?.toApi() - fun latestId(channel: String) = latest(channel)?.id?.value + fun latestId(channel: String) = latest(channel)?.id?.toApi() suspend fun archive( id: Int, @@ -172,4 +174,18 @@ internal class AnnouncementRepository { var url by Attachments.url var announcement by Announcement referencedOn Attachments.announcement } + + private fun Announcement.toApi() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachments.map { it.url }, + channel, + createdAt, + archivedAt, + level, + ) + + private fun Int.toApi() = APIResponseAnnouncementId(this) } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index ae4cf7ea..909cff8a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -2,21 +2,20 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.AnnouncementRepository import app.revanced.api.configuration.schema.APIAnnouncement -import app.revanced.api.configuration.schema.APIResponseAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.datetime.LocalDateTime internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)?.toApi() - fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()?.toApi() + fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) + fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() - fun latest(channel: String) = announcementRepository.latest(channel)?.toApi() - fun latest() = announcementRepository.latest()?.toApi() + fun latest(channel: String) = announcementRepository.latest(channel) + fun latest() = announcementRepository.latest() - suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() } - suspend fun all() = announcementRepository.all().map { it.toApi() } + suspend fun all(channel: String) = announcementRepository.all(channel) + suspend fun all() = announcementRepository.all() suspend fun new(new: APIAnnouncement) { announcementRepository.new(new) @@ -33,18 +32,4 @@ internal class AnnouncementService( suspend fun delete(id: Int) { announcementRepository.delete(id) } - - private fun AnnouncementRepository.Announcement.toApi() = APIResponseAnnouncement( - id.value, - author, - title, - content, - attachments.map { it.url }, - channel, - createdAt, - archivedAt, - level, - ) - - private fun Int.toApi() = APIResponseAnnouncementId(this) } From 89e2acfebb5e14f71d9ce3d962c5531070b7600e Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 05:17:45 +0200 Subject: [PATCH 77/81] feat: Use auth digest instead of basic auth --- .env.example | 5 ++-- .../api/configuration/Dependencies.kt | 5 ++-- .../app/revanced/api/configuration/OpenAPI.kt | 2 -- .../api/configuration/routes/ApiRoute.kt | 2 +- .../api/configuration/services/AuthService.kt | 27 ++++++++++++------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index a3cb37d3..1376e765 100644 --- a/.env.example +++ b/.env.example @@ -8,9 +8,8 @@ DB_URL=jdbc:h2:./persistence/revanced-api DB_USER= DB_PASSWORD= -# Basic authentication to issue JWT tokens -BASIC_USERNAME= -BASIC_PASSWORD= +# Digest auth to issue JWT tokens in the format SHA256("username:ReVanced:password") +AUTH_SHA256_DIGEST= # JWT configuration for authenticated API endpoints JWT_SECRET= diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 5256dd45..3f9f2858 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -118,10 +118,9 @@ fun Application.configureDependencies( val issuer = dotenv["JWT_ISSUER"] val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt() - val basicUsername = dotenv["BASIC_USERNAME"] - val basicPassword = dotenv["BASIC_PASSWORD"] + val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"] - AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) + AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString) } single { OldApiService( diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt index e1c637de..3c903215 100644 --- a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -9,7 +9,6 @@ import io.bkbn.kompendium.oas.component.Components import io.bkbn.kompendium.oas.info.Contact import io.bkbn.kompendium.oas.info.Info import io.bkbn.kompendium.oas.info.License -import io.bkbn.kompendium.oas.security.BasicAuth import io.bkbn.kompendium.oas.security.BearerAuth import io.bkbn.kompendium.oas.server.Server import io.ktor.server.application.* @@ -38,7 +37,6 @@ internal fun Application.configureOpenAPI() { components = Components( securitySchemes = mutableMapOf( "bearer" to BearerAuth(), - "basic" to BasicAuth(), ), ), ).apply { diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index fe2d3302..7cb56f2e 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -25,7 +25,7 @@ internal fun Route.rootRoute() { val authService = koinGet() rateLimit(RateLimitName("strong")) { - authenticate("basic") { + authenticate("auth-digest") { route("token") { installTokenRouteDocumentation() diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt index 8e4b8a99..a20e6d94 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt @@ -6,15 +6,23 @@ import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* import java.util.* +import kotlin.text.HexFormat import kotlin.time.Duration.Companion.minutes -internal class AuthService( +internal class AuthService private constructor( private val issuer: String, private val validityInMin: Int, private val jwtSecret: String, - private val basicUsername: String, - private val basicPassword: String, + private val authSHA256Digest: ByteArray, ) { + @OptIn(ExperimentalStdlibApi::class) + constructor(issuer: String, validityInMin: Int, jwtSecret: String, authSHA256DigestString: String) : this( + issuer, + validityInMin, + jwtSecret, + authSHA256DigestString.hexToByteArray(HexFormat.Default), + ) + val configureSecurity: Application.() -> Unit = { install(Authentication) { jwt("jwt") { @@ -26,13 +34,12 @@ internal class AuthService( validate { credential -> JWTPrincipal(credential.payload) } } - basic("basic") { - validate { credentials -> - if (credentials.name == basicUsername && credentials.password == basicPassword) { - UserIdPrincipal(credentials.name) - } else { - null - } + digest("auth-digest") { + realm = "ReVanced" + algorithmName = "SHA-256" + + digestProvider { _, _ -> + authSHA256Digest } } } From d4ac47194e51b3e708516150d1690a72830ab809 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 05:27:41 +0200 Subject: [PATCH 78/81] fix: Add missing OpenAPI docs --- .../api/configuration/routes/Announcements.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 70a6bc01..938d5565 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -92,6 +92,8 @@ internal fun Route.announcementsRoute() = route("announcements") { rateLimit(RateLimitName("strong")) { authenticate("jwt") { + installAnnouncementRouteDocumentation() + post { announcement -> announcementService.new(announcement) } @@ -136,6 +138,24 @@ internal fun Route.announcementsRoute() = route("announcements") { } } +private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute { + tags = setOf("Announcements") + + post = PostInfo.builder { + description("Create a new announcement") + summary("Create announcement") + request { + requestType() + description("The new announcement") + } + response { + description("When the announcement was created") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute { tags = setOf("Announcements") From a6008a2fb6d01fb577dcf357a1d698b6c1068b31 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 05:27:55 +0200 Subject: [PATCH 79/81] fix: Add missing auth realm --- .../revanced/api/configuration/services/AuthService.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt index a20e6d94..55e43951 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AuthService.kt @@ -26,12 +26,9 @@ internal class AuthService private constructor( val configureSecurity: Application.() -> Unit = { install(Authentication) { jwt("jwt") { - verifier( - JWT.require(Algorithm.HMAC256(jwtSecret)) - .withIssuer(issuer) - .build(), - ) - validate { credential -> JWTPrincipal(credential.payload) } + realm = "ReVanced" + + verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build()) } digest("auth-digest") { From e8c2488bc61793345c4b8171e520fb0127b34643 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 05:28:28 +0200 Subject: [PATCH 80/81] fix: Add uri to rate limiter request key --- .../kotlin/app/revanced/api/configuration/HTTP.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt index aa970c69..06c8a1fd 100644 --- a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -5,6 +5,7 @@ import io.ktor.server.application.* import io.ktor.server.plugins.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.ratelimit.* +import io.ktor.server.request.* import org.koin.ktor.ext.get import kotlin.time.Duration.Companion.minutes @@ -19,13 +20,19 @@ fun Application.configureHTTP() { } install(RateLimit) { - register(RateLimitName("weak")) { + fun rateLimit(name: String, block: RateLimitProviderConfig.() -> Unit) = register(RateLimitName(name)) { + requestKey { + it.request.uri + it.request.origin.remoteAddress + } + + block() + } + + rateLimit("weak") { rateLimiter(limit = 30, refillPeriod = 2.minutes) - requestKey { it.request.origin.remoteAddress } } - register(RateLimitName("strong")) { + rateLimit("strong") { rateLimiter(limit = 5, refillPeriod = 1.minutes) - requestKey { it.request.origin.remoteHost } } } } From 267546fc326780ce1a10a26574302cebd2332d2a Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 8 Jul 2024 14:02:25 +0200 Subject: [PATCH 81/81] build: Set JVM source and target version --- build.gradle.kts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 45481207..0ee566e0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin) alias(libs.plugins.ktor) @@ -35,6 +37,17 @@ ktor { } } +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } +} + repositories { mavenCentral() google()
-
- -
-
-
-

Application API

-
-
-
- -
-
-

Default

-
-
-
-

rootGet

-

-
-
-
-

-

Hello World!

-

-
-
/
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET\
--H "Accept: text/plain"\
-"http://0.0.0.0:8080/"
-
-
-
import io.swagger.client.*;
-import io.swagger.client.auth.*;
-import io.swagger.client.model.*;
-import io.swagger.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-
-    public static void main(String[] args) {
-        
-        DefaultApi apiInstance = new DefaultApi();
-        try {
-            'String' result = apiInstance.rootGet();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#rootGet");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
import io.swagger.client.api.DefaultApi;
-
-public class DefaultApiExample {
-
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        try {
-            'String' result = apiInstance.rootGet();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#rootGet");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-

-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance rootGetWithCompletionHandler: 
-              ^('String' output, NSError* error) {
-                            if (output) {
-                                NSLog(@"%@", output);
-                            }
-                            if (error) {
-                                NSLog(@"Error: %@", error);
-                            }
-                        }];
-
-
- -
-
var ApplicationApi = require('application_api');
-
-var api = new ApplicationApi.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.rootGet(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using IO.Swagger.Api;
-using IO.Swagger.Client;
-using IO.Swagger.Model;
-
-namespace Example
-{
-    public class rootGetExample
-    {
-        public void main()
-        {
-
-            var apiInstance = new DefaultApi();
-
-            try
-            {
-                'String' result = apiInstance.rootGet();
-                Debug.WriteLine(result);
-            }
-            catch (Exception e)
-            {
-                Debug.Print("Exception when calling DefaultApi.rootGet: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-$api_instance = new Swagger\Client\ApiDefaultApi();
-
-try {
-    $result = $api_instance->rootGet();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->rootGet: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::SwaggerClient::Configuration;
-use WWW::SwaggerClient::DefaultApi;
-
-my $api_instance = WWW::SwaggerClient::DefaultApi->new();
-
-eval { 
-    my $result = $api_instance->rootGet();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->rootGet: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import swagger_client
-from swagger_client.rest import ApiException
-from pprint import pprint
-
-# create an instance of the API class
-api_instance = swagger_client.DefaultApi()
-
-try: 
-    api_response = api_instance.root_get()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->rootGet: %s\n" % e)
-
-
- -

Parameters

- - - - - - -

Responses

-

Status: 200 - OK

- - - -
-
-
- -
- -
-
- -
-
-
-
-
- -
-
-