diff --git a/.github/workflows/formatting_testing.yml b/.github/workflows/formatting_testing.yml new file mode 100644 index 0000000..309b68a --- /dev/null +++ b/.github/workflows/formatting_testing.yml @@ -0,0 +1,41 @@ +name: Formatting check on pull requests + +on: + pull_request: + branches: + - dev + +jobs: + pre_commit: + runs-on: ubuntu-latest + environment: + name: testing + + steps: + - name: checkout repository + uses: actions/checkout@v2 + + - name: set up Python + uses: actions/setup-python@v2 + + - name: install pre-commit + run: python -m pip install pre-commit + shell: bash + + - name: pip freeze + run: python -m pip freeze --local + shell: bash + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + + - name: run pre-commit + run: | + FILES="${{ steps.changed-files.outputs.all_changed_files }}" + if [ -n "$FILES" ]; then + echo "$FILES" | xargs pre-commit run --files + else + echo "No files to check." + fi + shell: bash diff --git a/.github/workflows/publish_production.yml b/.github/workflows/publish_production.yml index 593d516..cdca733 100644 --- a/.github/workflows/publish_production.yml +++ b/.github/workflows/publish_production.yml @@ -5,6 +5,8 @@ on: # Publish `master` as Docker `prod` image. branches: - master + + workflow_dispatch: env: IMAGE_NAME: wachterbot @@ -61,24 +63,38 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} IMAGE_NAME: ${{ env.IMAGE_NAME }} ROOT_DIR: /home/${{ secrets.DO_USER }}/${{ env.PACKAGE_LABEL }} - POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + PERSISTENCE_DATABASE_URL: ${{ secrets.PERSISTENCE_DATABASE_URL }} + PERSISTENCE_DATABASE_USER: ${{ secrets.PERSISTENCE_DATABASE_USER }} + PERSISTENCE_DATABASE_PASSWORD: ${{ secrets.PERSISTENCE_DATABASE_PASSWORD }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + UPTRACE_DSN: ${{ secrets.UPTRACE_DSN }} TELEGRAM_ERROR_CHAT_ID: ${{ secrets.TELEGRAM_ERROR_CHAT_ID }} + TEAM_TELEGRAM_IDS: ${{ secrets.TEAM_TELEGRAM_IDS }} with: host: ${{ secrets.DO_HOST }} username: ${{ secrets.DO_USER }} passphrase: ${{ secrets.DO_SSH_KEY_PASSWORD }} key: ${{ secrets.DO_SSH_KEY }} port: ${{ secrets.DO_PORT }} - envs: GITHUB_USERNAME, GITHUB_TOKEN, ROOT_DIR, POSTGRES_URL, TELEGRAM_TOKEN + envs: GITHUB_USERNAME, GITHUB_TOKEN, ROOT_DIR, DATABASE_URL, PERSISTENCE_DATABASE_URL, TELEGRAM_TOKEN, TEAM_TELEGRAM_IDS, UPTRACE_DSN, DATABASE_USER, DATABASE_PASSWORD, PERSISTENCE_DATABASE_USER, PERSISTENCE_DATABASE_PASSWORD script: | - export CR_PAT=${{ secrets.GH_PAT_READ_PACKAGE }} - echo $CR_PAT | docker login ghcr.io -u $GITHUB_USERNAME + echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USERNAME" --password-stdin || exit 1 cd ${{ env.ROOT_DIR }} touch .env - echo POSTGRES_URL=${{ env.POSTGRES_URL }} >> .env + echo DATABASE_URL=${{ env.DATABASE_URL }} >> .env + echo DATABASE_USER=${{ env.DATABASE_USER }} >> .env + echo DATABASE_PASSWORD=${{ env.DATABASE_PASSWORD }} >> .env + echo PERSISTENCE_DATABASE_URL=${{ env.PERSISTENCE_DATABASE_URL }} >> .env + echo PERSISTENCE_DATABASE_USER=${{ env.PERSISTENCE_DATABASE_USER }} >> .env + echo PERSISTENCE_DATABASE_PASSWORD=${{ env.PERSISTENCE_DATABASE_PASSWORD }} >> .env echo TELEGRAM_TOKEN=${{ env.TELEGRAM_TOKEN }} >> .env + echo UPTRACE_DSN=${{ env.UPTRACE_DSN }} >> .env echo TELEGRAM_ERROR_CHAT_ID=${{ env.TELEGRAM_ERROR_CHAT_ID }} >> .env + echo DEBUG=False >> .env + echo TEAM_TELEGRAM_IDS=${{ env.TEAM_TELEGRAM_IDS }} >> .env docker-compose -f docker-compose.${{ env.PACKAGE_LABEL }}.yml stop docker-compose -f docker-compose.${{ env.PACKAGE_LABEL }}.yml pull docker-compose -f docker-compose.${{ env.PACKAGE_LABEL }}.yml up --force-recreate -d diff --git a/.github/workflows/publish_testing.yml b/.github/workflows/publish_testing.yml index 7c30910..a7947c8 100644 --- a/.github/workflows/publish_testing.yml +++ b/.github/workflows/publish_testing.yml @@ -6,6 +6,8 @@ on: branches: - dev + workflow_dispatch: + env: IMAGE_NAME: wachterbot GITHUB_USERNAME: alexeyqu @@ -61,24 +63,38 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} IMAGE_NAME: ${{ env.IMAGE_NAME }} ROOT_DIR: /home/${{ secrets.DO_USER }}/${{ env.PACKAGE_LABEL }} - POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + PERSISTENCE_DATABASE_URL: ${{ secrets.PERSISTENCE_DATABASE_URL }} + PERSISTENCE_DATABASE_USER: ${{ secrets.PERSISTENCE_DATABASE_USER }} + PERSISTENCE_DATABASE_PASSWORD: ${{ secrets.PERSISTENCE_DATABASE_PASSWORD }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} TELEGRAM_ERROR_CHAT_ID: ${{ secrets.TELEGRAM_ERROR_CHAT_ID }} + TEAM_TELEGRAM_IDS: ${{ secrets.TEAM_TELEGRAM_IDS }} + UPTRACE_DSN: ${{ secrets.UPTRACE_DSN }} with: host: ${{ secrets.DO_HOST }} username: ${{ secrets.DO_USER }} passphrase: ${{ secrets.DO_SSH_KEY_PASSWORD }} key: ${{ secrets.DO_SSH_KEY }} port: ${{ secrets.DO_PORT }} - envs: GITHUB_USERNAME, GITHUB_TOKEN, ROOT_DIR, POSTGRES_URL, TELEGRAM_TOKEN + envs: GITHUB_USERNAME, GITHUB_TOKEN, ROOT_DIR, DATABASE_URL, PERSISTENCE_DATABASE_URL, TELEGRAM_TOKEN, TEAM_TELEGRAM_IDS, UPTRACE_DSN, DATABASE_USER, DATABASE_PASSWORD, PERSISTENCE_DATABASE_USER, PERSISTENCE_DATABASE_PASSWORD script: | - export CR_PAT=${{ secrets.GH_PAT_READ_PACKAGE }} - echo $CR_PAT | docker login ghcr.io -u $GITHUB_USERNAME + echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USERNAME" --password-stdin || exit 1 cd ${{ env.ROOT_DIR }} touch .env - echo POSTGRES_URL=${{ env.POSTGRES_URL }} >> .env + echo DATABASE_URL=${{ env.DATABASE_URL }} >> .env + echo DATABASE_USER=${{ env.DATABASE_USER }} >> .env + echo DATABASE_PASSWORD=${{ env.DATABASE_PASSWORD }} >> .env + echo PERSISTENCE_DATABASE_URL=${{ env.PERSISTENCE_DATABASE_URL }} >> .env + echo PERSISTENCE_DATABASE_USER=${{ env.PERSISTENCE_DATABASE_USER }} >> .env + echo PERSISTENCE_DATABASE_PASSWORD=${{ env.PERSISTENCE_DATABASE_PASSWORD }} >> .env echo TELEGRAM_TOKEN=${{ env.TELEGRAM_TOKEN }} >> .env echo TELEGRAM_ERROR_CHAT_ID=${{ env.TELEGRAM_ERROR_CHAT_ID }} >> .env + echo UPTRACE_DSN=${{ env.UPTRACE_DSN }} >> .env + echo DEBUG=False >> .env + echo TEAM_TELEGRAM_IDS=${{ env.TEAM_TELEGRAM_IDS }} >> .env docker-compose -f docker-compose.${{ env.PACKAGE_LABEL }}.yml stop docker-compose -f docker-compose.${{ env.PACKAGE_LABEL }}.yml pull docker-compose -f docker-compose.${{ env.PACKAGE_LABEL }}.yml up --force-recreate -d diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1dcf6c9..1d926c4 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,39 +1,51 @@ -name: Testing on pull requests +name: CI on: pull_request: branches: - dev + jobs: - docker-compose-test: + tests: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - - name: Build and Run Docker Compose - run: | - docker-compose -f docker-compose.dev.yml up - - - name: Run pre-commit on Changed Files - run: | - # Get list of changed files - FILES=$(git diff --name-only HEAD^ HEAD) - - # Execute black inside container - docker exec wachter-dev bash -c "pipenv run pre-commit run black --all-files $FILES" - - # Check if black made any changes - if [ "$(git status --porcelain)" ]; then - echo "Changes were made by black. Please format your code before pushing." - exit 1 - fi - - - name: Check Docker logs for errors - run: | - logs=$(docker logs YOUR_CONTAINER_NAME_OR_ID) - if echo $logs | grep -i "exited with code"; then - echo "Found errors in Docker logs" - exit 1 - fi + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 # required by Pipfile + + - name: Cache pipenv dependencies + uses: actions/cache@v2 + with: + path: ~/.local/share/virtualenvs + key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }} + restore-keys: | + ${{ runner.os }}-pipenv- + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('Pipfile.lock') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install pipenv + run: pip install pipenv + + - name: Install dependencies + run: pipenv install + + - name: Install pytest explicitly (failes without) + run: pipenv run pip install pytest + + - name: Install asyncio (we doing it in Dockerfile) + run: pipenv run pip install 'httpcore[asyncio]' + + - name: Run tests + run: pipenv run python -m pytest ./test -x -s + shell: bash diff --git a/.gitignore b/.gitignore index 4823eef..d7e4dda 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ .DS_Store .vscode data/ +persistent_storage.pickle diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37af784..e5f5c36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,9 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - -- repo: local +- repo: https://github.com/psf/black + rev: 23.9.1 hooks: - id: black - name: black - entry: bash -c 'pipenv run black $@' -- - language: system - types: [python] - args: [] exclude: ^(.+)/migrations/.*\.py - \ No newline at end of file + diff --git a/Dockerfile b/Dockerfile index b040827..45304e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,16 @@ -FROM python:3.6 +FROM python:3.9 RUN pip install "setuptools<46" && pip install pipenv COPY Pipfile /Pipfile COPY Pipfile.lock /Pipfile.lock -RUN pipenv install --system +RUN pip install "httpcore[asyncio]" + +RUN pipenv install --deploy --system COPY . /app WORKDIR /app -CMD alembic upgrade head && python -m wachter.bot +RUN ["chmod", "+x", "/app/entrypoint.sh"] +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/Pipfile b/Pipfile index 5023052..1742b3b 100644 --- a/Pipfile +++ b/Pipfile @@ -4,16 +4,29 @@ verify_ssl = true name = "pypi" [packages] -"psycopg2-binary" = "*" -python-telegram-bot = "==10.1.0" sqlalchemy = "*" alembic = "*" pre-commit = "*" pylint = "*" black = "*" +python-telegram-handler = "*" +pytest = "*" +pytest-mock = "*" +httpcore = {extras = ["asyncio"], version = "*"} +asyncio = "*" +asyncpg = "*" +python-telegram-bot = {extras = ["job-queue"], version = "*"} +ptbcontrib = {extras = ["ptb_jobstores_sqlalchemy"], git = "git+https://github.com/python-telegram-bot/ptbcontrib.git@main"} +"psycopg2-binary" = "*" # need for ptb_jobstores_sqlalchemy +pytest-asyncio = "*" +aiosqlite = "*" +grpcio = "*" +opentelemetry-sdk = "*" +opentelemetry-exporter-otlp = "*" +opentelemetry-api = "*" [dev-packages] "autopep8" = "*" [requires] -python_version = "3.6" \ No newline at end of file +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index d0c2e28..b9eab33 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "541eca1d9fe5100c2cfa0f75ce996e3626a86be59c15d9ca11387ed53d496d0c" + "sha256": "e903d17cd9d86285f15e7184fec65b13946f3869872b68f900a5f8df98e9a1f9" }, "pipfile-spec": 6, "requires": { - "python_version": "3.6" + "python_version": "3.9" }, "sources": [ { @@ -16,340 +16,605 @@ ] }, "default": { + "aiosqlite": { + "hashes": [ + "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", + "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, "alembic": { "hashes": [ - "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b", - "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58" + "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2", + "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e" ], "index": "pypi", - "version": "==1.7.7" + "markers": "python_version >= '3.8'", + "version": "==1.13.3" + }, + "anyio": { + "hashes": [ + "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", + "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" + ], + "version": "==4.6.2.post1" + }, + "apscheduler": { + "hashes": [ + "sha256:e6df071b27d9be898e486bc7940a7be50b4af2e9da7c08f0744a96d4bd4cef4a", + "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661" + ], + "version": "==3.10.4" }, "astroid": { "hashes": [ - "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b", - "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946" + "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d", + "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8" ], - "markers": "python_full_version >= '3.6.2'", - "version": "==2.11.7" + "markers": "python_full_version >= '3.9.0'", + "version": "==3.3.5" + }, + "async-timeout": { + "hashes": [ + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + ], + "markers": "python_full_version < '3.12.0'", + "version": "==4.0.3" + }, + "asyncio": { + "hashes": [ + "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", + "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de", + "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c", + "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d" + ], + "index": "pypi", + "version": "==3.4.3" + }, + "asyncpg": { + "hashes": [ + "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9", + "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7", + "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548", + "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23", + "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3", + "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675", + "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe", + "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175", + "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83", + "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385", + "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da", + "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106", + "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870", + "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449", + "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc", + "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178", + "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9", + "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b", + "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169", + "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610", + "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772", + "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2", + "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c", + "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb", + "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac", + "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408", + "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22", + "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb", + "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02", + "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59", + "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8", + "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3", + "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e", + "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4", + "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364", + "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f", + "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775", + "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3", + "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090", + "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810", + "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==0.29.0" }, "black": { "hashes": [ - "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411", - "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c", - "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497", - "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e", - "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342", - "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27", - "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41", - "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab", - "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5", - "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16", - "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e", - "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c", - "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe", - "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3", - "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec", - "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3", - "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd", - "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c", - "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4", - "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90", - "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869", - "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747", - "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875" + "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", + "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", + "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", + "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", + "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", + "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", + "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", + "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", + "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", + "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", + "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", + "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", + "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", + "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", + "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", + "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", + "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", + "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", + "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", + "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", + "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", + "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e" ], "index": "pypi", - "version": "==22.8.0" + "markers": "python_version >= '3.9'", + "version": "==24.10.0" }, "certifi": { "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2023.7.22" + "version": "==2024.8.30" }, "cfgv": { "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.4.0" }, "click": { "hashes": [ - "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", - "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.4" + "markers": "python_version >= '3.7'", + "version": "==8.1.7" }, - "dataclasses": { + "deprecated": { "hashes": [ - "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", - "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97" + "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", + "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" ], - "markers": "python_version < '3.7'", - "version": "==0.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.2.14" }, "dill": { "hashes": [ - "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f", - "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675" + "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", + "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c" ], - "markers": "python_version >= '2.7' and python_version != '3.0'", - "version": "==0.3.4" + "markers": "python_version < '3.11'", + "version": "==0.3.9" }, "distlib": { "hashes": [ - "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", - "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" + "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", + "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" ], - "version": "==0.3.7" + "version": "==0.3.9" + }, + "exceptiongroup": { + "hashes": [ + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.2" }, "filelock": { "hashes": [ - "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06", - "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], - "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "markers": "python_version >= '3.8'", + "version": "==3.16.1" }, - "future": { + "googleapis-common-protos": { "hashes": [ - "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307" + "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", + "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.18.3" + "markers": "python_version >= '3.7'", + "version": "==1.65.0" }, "greenlet": { "hashes": [ - "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", - "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", - "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1", - "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", - "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", - "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", - "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", - "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", - "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", - "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", - "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", - "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", - "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", - "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", - "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", - "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", - "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", - "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", - "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", - "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", - "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", - "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", - "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", - "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", - "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", - "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", - "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", - "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", - "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417", - "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", - "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", - "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", - "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", - "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", - "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", - "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", - "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", - "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", - "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", - "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", - "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", - "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", - "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", - "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", - "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", - "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", - "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", - "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", - "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", - "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", - "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", - "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", - "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47", - "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", - "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", - "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c", - "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", - "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", - "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", - "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", - "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", - "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", - "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", - "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" - ], - "markers": "python_version >= '3' 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'))))))", - "version": "==2.0.2" + "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", + "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", + "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", + "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", + "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", + "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", + "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", + "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", + "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", + "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", + "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", + "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", + "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", + "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", + "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", + "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", + "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", + "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", + "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", + "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", + "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", + "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", + "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", + "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", + "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", + "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", + "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", + "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", + "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", + "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", + "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", + "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", + "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", + "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", + "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", + "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", + "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", + "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", + "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", + "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", + "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", + "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", + "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", + "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", + "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", + "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", + "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", + "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", + "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", + "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", + "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", + "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", + "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", + "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", + "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", + "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", + "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", + "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", + "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", + "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", + "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", + "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", + "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", + "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", + "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", + "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", + "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", + "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", + "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", + "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", + "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", + "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", + "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" + ], + "markers": "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')))))", + "version": "==3.1.1" + }, + "grpcio": { + "hashes": [ + "sha256:014dfc020e28a0d9be7e93a91f85ff9f4a87158b7df9952fe23cc42d29d31e1e", + "sha256:0892dd200ece4822d72dd0952f7112c542a487fc48fe77568deaaa399c1e717d", + "sha256:0bb94e66cd8f0baf29bd3184b6aa09aeb1a660f9ec3d85da615c5003154bc2bf", + "sha256:0c69bf11894cad9da00047f46584d5758d6ebc9b5950c0dc96fec7e0bce5cde9", + "sha256:15c05a26a0f7047f720da41dc49406b395c1470eef44ff7e2c506a47ac2c0591", + "sha256:16724ffc956ea42967f5758c2f043faef43cb7e48a51948ab593570570d1e68b", + "sha256:227316b5631260e0bef8a3ce04fa7db4cc81756fea1258b007950b6efc90c05d", + "sha256:2b7183c80b602b0ad816315d66f2fb7887614ead950416d60913a9a71c12560d", + "sha256:2f55c1e0e2ae9bdd23b3c63459ee4c06d223b68aeb1961d83c48fb63dc29bc03", + "sha256:30d47dbacfd20cbd0c8be9bfa52fdb833b395d4ec32fe5cff7220afc05d08571", + "sha256:323741b6699cd2b04a71cb38f502db98f90532e8a40cb675393d248126a268af", + "sha256:3840994689cc8cbb73d60485c594424ad8adb56c71a30d8948d6453083624b52", + "sha256:391df8b0faac84d42f5b8dfc65f5152c48ed914e13c522fd05f2aca211f8bfad", + "sha256:42199e704095b62688998c2d84c89e59a26a7d5d32eed86d43dc90e7a3bd04aa", + "sha256:54d16383044e681f8beb50f905249e4e7261dd169d4aaf6e52eab67b01cbbbe2", + "sha256:5a1e03c3102b6451028d5dc9f8591131d6ab3c8a0e023d94c28cb930ed4b5f81", + "sha256:62492bd534979e6d7127b8a6b29093161a742dee3875873e01964049d5250a74", + "sha256:662c8e105c5e5cee0317d500eb186ed7a93229586e431c1bf0c9236c2407352c", + "sha256:682968427a63d898759474e3b3178d42546e878fdce034fd7474ef75143b64e3", + "sha256:74b900566bdf68241118f2918d312d3bf554b2ce0b12b90178091ea7d0a17b3d", + "sha256:77196216d5dd6f99af1c51e235af2dd339159f657280e65ce7e12c1a8feffd1d", + "sha256:7f200aca719c1c5dc72ab68be3479b9dafccdf03df530d137632c534bb6f1ee3", + "sha256:7fc1d2b9fd549264ae585026b266ac2db53735510a207381be509c315b4af4e8", + "sha256:82e5bd4b67b17c8c597273663794a6a46a45e44165b960517fe6d8a2f7f16d23", + "sha256:8c9a35b8bc50db35ab8e3e02a4f2a35cfba46c8705c3911c34ce343bd777813a", + "sha256:985b2686f786f3e20326c4367eebdaed3e7aa65848260ff0c6644f817042cb15", + "sha256:9d75641a2fca9ae1ae86454fd25d4c298ea8cc195dbc962852234d54a07060ad", + "sha256:a4e95e43447a02aa603abcc6b5e727d093d161a869c83b073f50b9390ecf0fa8", + "sha256:a6b9a5c18863fd4b6624a42e2712103fb0f57799a3b29651c0e5b8119a519d65", + "sha256:aa8d025fae1595a207b4e47c2e087cb88d47008494db258ac561c00877d4c8f8", + "sha256:ac11ecb34a86b831239cc38245403a8de25037b448464f95c3315819e7519772", + "sha256:ae6de510f670137e755eb2a74b04d1041e7210af2444103c8c95f193340d17ee", + "sha256:b2a44e572fb762c668e4812156b81835f7aba8a721b027e2d4bb29fb50ff4d33", + "sha256:b6eb68493a05d38b426604e1dc93bfc0137c4157f7ab4fac5771fd9a104bbaa6", + "sha256:b9bca3ca0c5e74dea44bf57d27e15a3a3996ce7e5780d61b7c72386356d231db", + "sha256:bd79929b3bb96b54df1296cd3bf4d2b770bd1df6c2bdf549b49bab286b925cdc", + "sha256:c4c425f440fb81f8d0237c07b9322fc0fb6ee2b29fbef5f62a322ff8fcce240d", + "sha256:cb204a742997277da678611a809a8409657b1398aaeebf73b3d9563b7d154c13", + "sha256:cf51d28063338608cd8d3cd64677e922134837902b70ce00dad7f116e3998210", + "sha256:cfd9306511fdfc623a1ba1dc3bc07fbd24e6cfbe3c28b4d1e05177baa2f99617", + "sha256:cff8e54d6a463883cda2fab94d2062aad2f5edd7f06ae3ed030f2a74756db365", + "sha256:d01793653248f49cf47e5695e0a79805b1d9d4eacef85b310118ba1dfcd1b955", + "sha256:d4ea4509d42c6797539e9ec7496c15473177ce9abc89bc5c71e7abe50fc25737", + "sha256:d90cfdafcf4b45a7a076e3e2a58e7bc3d59c698c4f6470b0bb13a4d869cf2273", + "sha256:e090b2553e0da1c875449c8e75073dd4415dd71c9bde6a406240fdf4c0ee467c", + "sha256:e91d154689639932305b6ea6f45c6e46bb51ecc8ea77c10ef25aa77f75443ad4", + "sha256:eef1dce9d1a46119fd09f9a992cf6ab9d9178b696382439446ca5f399d7b96fe", + "sha256:efe32b45dd6d118f5ea2e5deaed417d8a14976325c93812dd831908522b402c9", + "sha256:f4d613fbf868b2e2444f490d18af472ccb47660ea3df52f068c9c8801e1f3e85", + "sha256:f55f077685f61f0fbd06ea355142b71e47e4a26d2d678b3ba27248abfe67163a", + "sha256:f623c57a5321461c84498a99dddf9d13dac0e40ee056d884d6ec4ebcab647a78", + "sha256:f6bd2ab135c64a4d1e9e44679a616c9bc944547357c830fafea5c3caa3de5153", + "sha256:f95e15db43e75a534420e04822df91f645664bf4ad21dfaad7d51773c80e6bb4", + "sha256:fd6bc27861e460fe28e94226e3673d46e294ca4673d46b224428d197c5935e69", + "sha256:fe89295219b9c9e47780a0f1c75ca44211e706d1c598242249fe717af3385ec8" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.67.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "httpcore": { + "extras": [ + "asyncio" + ], + "hashes": [ + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.6" + }, + "httpx": { + "hashes": [ + "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", + "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" + ], + "markers": "python_version >= '3.8'", + "version": "==0.27.2" }, "identify": { "hashes": [ - "sha256:6b4b5031f69c48bf93a646b90de9b381c6b5f560df4cbe0ed3cf7650ae741e4d", - "sha256:aa68609c7454dbcaae60a01ff6b8df1de9b39fe6e50b1f6107ec81dcda624aa6" + "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", + "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" + ], + "markers": "python_version >= '3.8'", + "version": "==2.6.1" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==2.4.4" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "importlib-metadata": { "hashes": [ - "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e", - "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668" + "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", + "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], - "markers": "python_version < '3.8'", - "version": "==4.8.3" + "markers": "python_version >= '3.8'", + "version": "==8.4.0" }, - "importlib-resources": { + "iniconfig": { "hashes": [ - "sha256:203d70dda34cfbfbb42324a8d4211196e7d3e858de21a5eb68c6d1cdd99e4e98", - "sha256:ae35ed1cfe8c0d6c1a53ecd168167f01fa93b893d51a62cdf23aea044c67211b" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], - "markers": "python_version < '3.7'", - "version": "==5.2.3" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "isort": { "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" - ], - "markers": "python_full_version >= '3.6.1' and python_version < '4.0'", - "version": "==5.10.1" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", - "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", - "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", - "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", - "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", - "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", - "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", - "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", - "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", - "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", - "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", - "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", - "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", - "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", - "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", - "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", - "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", - "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", - "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", - "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", - "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", - "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", - "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", - "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", - "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", - "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", - "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", - "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", - "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", - "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", - "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", - "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", - "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", - "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", - "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", - "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", - "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], - "markers": "python_version >= '3.6'", - "version": "==1.7.1" + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" }, "mako": { "hashes": [ - "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2", - "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57" + "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a", + "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.6" + "markers": "python_version >= '3.8'", + "version": "==1.3.5" }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396", + "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38", + "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a", + "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8", + "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b", + "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad", + "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a", + "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a", + "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da", + "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6", + "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8", + "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344", + "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a", + "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8", + "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5", + "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7", + "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170", + "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132", + "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9", + "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd", + "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9", + "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346", + "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc", + "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589", + "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5", + "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915", + "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295", + "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453", + "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea", + "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b", + "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d", + "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b", + "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4", + "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b", + "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7", + "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf", + "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f", + "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91", + "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd", + "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50", + "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b", + "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583", + "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a", + "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984", + "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c", + "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c", + "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25", + "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa", + "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4", + "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3", + "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97", + "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1", + "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd", + "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772", + "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a", + "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729", + "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca", + "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6", + "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635", + "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b", + "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.9'", + "version": "==3.0.1" }, "mccabe": { "hashes": [ @@ -369,417 +634,584 @@ }, "nodeenv": { "hashes": [ - "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", - "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" ], - "version": "==1.6.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.9.1" + }, + "opentelemetry-api": { + "hashes": [ + "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7", + "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-exporter-otlp": { + "hashes": [ + "sha256:4a599459e623868cc95d933c301199c2367e530f089750e115599fccd67cb2a1", + "sha256:7688791cbdd951d71eb6445951d1cfbb7b6b2d7ee5948fac805d404802931145" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-exporter-otlp-proto-common": { + "hashes": [ + "sha256:159d27cf49f359e3798c4c3eb8da6ef4020e292571bd8c5604a2a573231dd5c8", + "sha256:675db7fffcb60946f3a5c43e17d1168a3307a94a930ecf8d2ea1f286f3d4f79a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-exporter-otlp-proto-grpc": { + "hashes": [ + "sha256:56b5bbd5d61aab05e300d9d62a6b3c134827bbd28d0b12f2649c2da368006c9e", + "sha256:af6f72f76bcf425dfb5ad11c1a6d6eca2863b91e63575f89bb7b4b55099d968f" + ], + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-exporter-otlp-proto-http": { + "hashes": [ + "sha256:2103479092d8eb18f61f3fbff084f67cc7f2d4a7d37e75304b8b56c1d09ebef5", + "sha256:688027575c9da42e179a69fe17e2d1eba9b14d81de8d13553a21d3114f3b4d75" + ], + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-proto": { + "hashes": [ + "sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6", + "sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace" + ], + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-sdk": { + "hashes": [ + "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d", + "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.27.0" + }, + "opentelemetry-semantic-conventions": { + "hashes": [ + "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a", + "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f" + ], + "markers": "python_version >= '3.8'", + "version": "==0.48b0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "version": "==0.9.0" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "platformdirs": { "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" + "markers": "python_version >= '3.8'", + "version": "==4.3.6" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" }, "pre-commit": { "hashes": [ - "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616", - "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a" + "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", + "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878" ], "index": "pypi", - "version": "==2.17.0" + "markers": "python_version >= '3.9'", + "version": "==4.0.1" + }, + "protobuf": { + "hashes": [ + "sha256:0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41", + "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea", + "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8", + "sha256:7a183f592dc80aa7c8da7ad9e55091c4ffc9497b3054452d629bb85fa27c2a45", + "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584", + "sha256:919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d", + "sha256:98d8d8aa50de6a2747efd9cceba361c9034050ecce3e09136f90de37ddba66e1", + "sha256:abe32aad8561aa7cc94fc7ba4fdef646e576983edb94a73381b03c53728a626f", + "sha256:b0234dd5a03049e4ddd94b93400b67803c823cfc405689688f59b34e0742381a", + "sha256:b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173", + "sha256:fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331" + ], + "markers": "python_version >= '3.8'", + "version": "==4.25.5" }, "psycopg2-binary": { "hashes": [ - "sha256:00d8db270afb76f48a499f7bb8fa70297e66da67288471ca873db88382850bf4", - "sha256:024eaeb2a08c9a65cd5f94b31ace1ee3bb3f978cd4d079406aef85169ba01f08", - "sha256:094af2e77a1976efd4956a031028774b827029729725e136514aae3cdf49b87b", - "sha256:1011eeb0c51e5b9ea1016f0f45fa23aca63966a4c0afcf0340ccabe85a9f65bd", - "sha256:11abdbfc6f7f7dea4a524b5f4117369b0d757725798f1593796be6ece20266cb", - "sha256:122641b7fab18ef76b18860dd0c772290566b6fb30cc08e923ad73d17461dc63", - "sha256:17cc17a70dfb295a240db7f65b6d8153c3d81efb145d76da1e4a096e9c5c0e63", - "sha256:18f12632ab516c47c1ac4841a78fddea6508a8284c7cf0f292cb1a523f2e2379", - "sha256:1b918f64a51ffe19cd2e230b3240ba481330ce1d4b7875ae67305bd1d37b041c", - "sha256:1c31c2606ac500dbd26381145684d87730a2fac9a62ebcfbaa2b119f8d6c19f4", - "sha256:26484e913d472ecb6b45937ea55ce29c57c662066d222fb0fbdc1fab457f18c5", - "sha256:2993ccb2b7e80844d534e55e0f12534c2871952f78e0da33c35e648bf002bbff", - "sha256:2b04da24cbde33292ad34a40db9832a80ad12de26486ffeda883413c9e1b1d5e", - "sha256:2dec5a75a3a5d42b120e88e6ed3e3b37b46459202bb8e36cd67591b6e5feebc1", - "sha256:2df562bb2e4e00ee064779902d721223cfa9f8f58e7e52318c97d139cf7f012d", - "sha256:3fbb1184c7e9d28d67671992970718c05af5f77fc88e26fd7136613c4ece1f89", - "sha256:42a62ef0e5abb55bf6ffb050eb2b0fcd767261fa3faf943a4267539168807522", - "sha256:4ecc15666f16f97709106d87284c136cdc82647e1c3f8392a672616aed3c7151", - "sha256:4eec5d36dbcfc076caab61a2114c12094c0b7027d57e9e4387b634e8ab36fd44", - "sha256:4fe13712357d802080cfccbf8c6266a3121dc0e27e2144819029095ccf708372", - "sha256:51d1b42d44f4ffb93188f9b39e6d1c82aa758fdb8d9de65e1ddfe7a7d250d7ad", - "sha256:59f7e9109a59dfa31efa022e94a244736ae401526682de504e87bd11ce870c22", - "sha256:62cb6de84d7767164a87ca97e22e5e0a134856ebcb08f21b621c6125baf61f16", - "sha256:642df77484b2dcaf87d4237792246d8068653f9e0f5c025e2c692fc56b0dda70", - "sha256:6822c9c63308d650db201ba22fe6648bd6786ca6d14fdaf273b17e15608d0852", - "sha256:692df8763b71d42eb8343f54091368f6f6c9cfc56dc391858cdb3c3ef1e3e584", - "sha256:6d92e139ca388ccfe8c04aacc163756e55ba4c623c6ba13d5d1595ed97523e4b", - "sha256:7952807f95c8eba6a8ccb14e00bf170bb700cafcec3924d565235dffc7dc4ae8", - "sha256:7db7b9b701974c96a88997d458b38ccb110eba8f805d4b4f74944aac48639b42", - "sha256:81d5dd2dd9ab78d31a451e357315f201d976c131ca7d43870a0e8063b6b7a1ec", - "sha256:8a136c8aaf6615653450817a7abe0fc01e4ea720ae41dfb2823eccae4b9062a3", - "sha256:8a7968fd20bd550431837656872c19575b687f3f6f98120046228e451e4064df", - "sha256:8c721ee464e45ecf609ff8c0a555018764974114f671815a0a7152aedb9f3343", - "sha256:8f309b77a7c716e6ed9891b9b42953c3ff7d533dc548c1e33fddc73d2f5e21f9", - "sha256:8f94cb12150d57ea433e3e02aabd072205648e86f1d5a0a692d60242f7809b15", - "sha256:95a7a747bdc3b010bb6a980f053233e7610276d55f3ca506afff4ad7749ab58a", - "sha256:9b0c2b466b2f4d89ccc33784c4ebb1627989bd84a39b79092e560e937a11d4ac", - "sha256:9dcfd5d37e027ec393a303cc0a216be564b96c80ba532f3d1e0d2b5e5e4b1e6e", - "sha256:a5ee89587696d808c9a00876065d725d4ae606f5f7853b961cdbc348b0f7c9a1", - "sha256:a6a8b575ac45af1eaccbbcdcf710ab984fd50af048fe130672377f78aaff6fc1", - "sha256:ac83ab05e25354dad798401babaa6daa9577462136ba215694865394840e31f8", - "sha256:ad26d4eeaa0d722b25814cce97335ecf1b707630258f14ac4d2ed3d1d8415265", - "sha256:ad5ec10b53cbb57e9a2e77b67e4e4368df56b54d6b00cc86398578f1c635f329", - "sha256:c82986635a16fb1fa15cd5436035c88bc65c3d5ced1cfaac7f357ee9e9deddd4", - "sha256:ced63c054bdaf0298f62681d5dcae3afe60cbae332390bfb1acf0e23dcd25fc8", - "sha256:d0b16e5bb0ab78583f0ed7ab16378a0f8a89a27256bb5560402749dbe8a164d7", - "sha256:dbbc3c5d15ed76b0d9db7753c0db40899136ecfe97d50cbde918f630c5eb857a", - "sha256:ded8e15f7550db9e75c60b3d9fcbc7737fea258a0f10032cdb7edc26c2a671fd", - "sha256:e02bc4f2966475a7393bd0f098e1165d470d3fa816264054359ed4f10f6914ea", - "sha256:e5666632ba2b0d9757b38fc17337d84bdf932d38563c5234f5f8c54fd01349c9", - "sha256:ea5f8ee87f1eddc818fc04649d952c526db4426d26bab16efbe5a0c52b27d6ab", - "sha256:eb1c0e682138f9067a58fc3c9a9bf1c83d8e08cfbee380d858e63196466d5c86", - "sha256:eb3b8d55924a6058a26db69fb1d3e7e32695ff8b491835ba9f479537e14dcf9f", - "sha256:ee919b676da28f78f91b464fb3e12238bd7474483352a59c8a16c39dfc59f0c5", - "sha256:f02f4a72cc3ab2565c6d9720f0343cb840fb2dc01a2e9ecb8bc58ccf95dc5c06", - "sha256:f4f37bbc6588d402980ffbd1f3338c871368fb4b1cfa091debe13c68bb3852b3", - "sha256:f8651cf1f144f9ee0fa7d1a1df61a9184ab72962531ca99f077bbdcba3947c58", - "sha256:f955aa50d7d5220fcb6e38f69ea126eafecd812d96aeed5d5f3597f33fad43bb", - "sha256:fc10da7e7df3380426521e8c1ed975d22df678639da2ed0ec3244c3dc2ab54c8", - "sha256:fdca0511458d26cf39b827a663d7d87db6f32b93efc22442a742035728603d5f" + "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", + "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", + "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", + "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", + "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", + "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", + "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", + "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", + "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", + "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", + "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", + "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", + "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", + "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", + "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", + "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", + "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", + "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", + "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", + "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", + "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", + "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", + "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", + "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", + "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", + "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", + "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", + "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", + "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", + "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", + "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", + "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", + "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", + "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", + "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", + "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", + "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", + "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", + "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", + "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", + "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", + "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", + "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", + "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", + "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", + "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", + "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", + "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", + "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", + "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", + "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", + "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", + "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", + "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", + "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", + "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", + "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", + "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", + "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", + "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", + "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", + "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", + "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", + "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", + "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", + "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" ], "index": "pypi", - "version": "==2.9.7" + "markers": "python_version >= '3.8'", + "version": "==2.9.10" + }, + "ptbcontrib": { + "extras": [ + "ptb_jobstores_sqlalchemy" + ], + "git": "git+https://github.com/python-telegram-bot/ptbcontrib.git@main", + "ref": "df735c17b4e00a65d4a6057f942f9be04e6ed786" }, "pylint": { "hashes": [ - "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731", - "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526" + "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9", + "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e" + ], + "index": "pypi", + "markers": "python_full_version >= '3.9.0'", + "version": "==3.3.1" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.3.3" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", + "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.24.0" + }, + "pytest-mock": { + "hashes": [ + "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", + "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" ], "index": "pypi", - "version": "==2.13.9" + "markers": "python_version >= '3.8'", + "version": "==3.14.0" }, "python-telegram-bot": { + "extras": [ + "job-queue" + ], "hashes": [ - "sha256:725a28f77a04d056559015963a21eacf5d2d1f1722192237284828b7cc437465", - "sha256:ca2f8a44ddef7271477e16f4986647fa90ef4df5b55a7953e53b9c9d2672f639" + "sha256:8b2b37836c3ff9c2924e990474a1c4731df21b1668acebff5099f475666426c6", + "sha256:f2d6431bf154a53f40cdfc6c1d492a66102c0e4938709f6d8202bcd951c840cb" + ], + "markers": "python_version >= '3.8'", + "version": "==21.6" + }, + "python-telegram-handler": { + "hashes": [ + "sha256:f6e9ca60e15fa4e4595e323cc57362fe20cca3ca16e06158ad726caa48b3b16e" ], "index": "pypi", - "version": "==10.1.0" + "version": "==2.2.1" + }, + "pytz": { + "hashes": [ + "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" + ], + "version": "==2024.2" }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, - "setuptools": { + "requests": { "hashes": [ - "sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373", - "sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.6'", - "version": "==59.6.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sniffio": { + "hashes": [ + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" }, "sqlalchemy": { "hashes": [ - "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71", - "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9", - "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557", - "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1", - "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77", - "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d", - "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf", - "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6", - "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb", - "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a", - "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4", - "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b", - "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d", - "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729", - "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f", - "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264", - "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c", - "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87", - "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4", - "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1", - "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91", - "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55", - "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09", - "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e", - "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd", - "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0", - "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e", - "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30", - "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de", - "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f", - "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95", - "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d", - "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d", - "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5", - "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294", - "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532", - "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c", - "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa" + "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", + "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", + "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2", + "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", + "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", + "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", + "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", + "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575", + "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", + "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", + "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", + "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", + "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", + "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971", + "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", + "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", + "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", + "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d", + "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", + "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", + "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", + "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", + "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", + "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", + "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", + "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", + "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", + "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", + "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", + "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", + "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", + "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", + "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", + "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", + "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", + "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", + "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", + "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", + "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", + "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", + "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", + "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", + "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c", + "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b", + "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", + "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", + "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", + "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", + "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", + "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", + "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", + "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", + "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb", + "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", + "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", + "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", + "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53" ], "index": "pypi", - "version": "==1.4.49" + "markers": "python_version >= '3.7'", + "version": "==2.0.36" }, - "toml": { + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" + "markers": "python_version < '3.11'", + "version": "==2.0.2" }, - "tomli": { + "tomlkit": { "hashes": [ - "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", - "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" - ], - "markers": "python_full_version < '3.11.0a7'", - "version": "==1.2.3" - }, - "typed-ast": { - "hashes": [ - "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", - "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", - "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", - "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c", - "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d", - "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", - "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", - "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5", - "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", - "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", - "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", - "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5", - "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", - "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", - "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", - "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", - "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", - "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", - "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", - "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", - "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", - "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", - "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", - "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", - "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", - "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", - "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa", - "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", - "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", - "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", - "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", - "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", - "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", - "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", - "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", - "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", - "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", - "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", - "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f", - "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", - "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2" - ], - "markers": "python_version < '3.8' and implementation_name == 'cpython'", - "version": "==1.5.5" + "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", + "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79" + ], + "markers": "python_version >= '3.8'", + "version": "==0.13.2" }, "typing-extensions": { "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "tzlocal": { + "hashes": [ + "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", + "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e" ], - "markers": "python_version < '3.10'", - "version": "==4.1.1" + "markers": "python_version >= '3.8'", + "version": "==5.2" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.3" }, "virtualenv": { "hashes": [ - "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db", - "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3" + "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2", + "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655" ], - "markers": "python_version >= '3.6'", - "version": "==20.16.2" + "markers": "python_version >= '3.8'", + "version": "==20.27.0" }, "wrapt": { "hashes": [ - "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", - "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", - "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", - "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", - "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", - "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", - "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", - "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", - "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", - "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", - "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", - "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", - "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", - "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", - "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", - "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", - "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", - "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", - "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", - "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", - "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", - "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", - "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", - "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", - "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", - "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", - "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", - "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", - "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", - "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", - "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", - "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", - "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", - "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", - "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", - "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", - "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", - "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", - "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", - "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", - "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", - "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", - "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", - "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", - "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", - "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", - "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", - "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", - "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", - "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", - "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", - "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", - "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", - "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", - "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", - "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", - "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", - "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", - "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", - "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", - "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", - "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", - "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", - "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", - "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", - "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", - "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", - "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", - "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", - "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", - "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", - "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", - "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", - "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", - "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.15.0" + "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", + "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", + "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", + "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", + "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", + "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", + "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", + "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", + "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", + "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", + "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", + "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", + "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", + "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", + "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", + "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", + "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", + "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", + "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", + "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", + "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", + "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", + "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", + "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", + "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", + "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", + "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", + "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", + "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", + "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", + "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", + "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", + "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", + "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", + "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", + "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", + "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", + "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", + "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", + "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", + "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", + "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", + "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", + "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", + "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", + "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", + "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", + "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", + "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", + "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", + "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", + "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", + "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", + "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", + "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", + "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", + "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", + "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", + "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", + "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", + "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", + "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", + "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", + "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", + "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", + "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", + "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", + "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", + "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", + "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.16.0" }, "zipp": { "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" + "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" ], - "markers": "python_version < '3.10'", - "version": "==3.6.0" + "markers": "python_version >= '3.8'", + "version": "==3.20.2" } }, "develop": { "autopep8": { "hashes": [ - "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb", - "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c" + "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", + "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d" ], "index": "pypi", - "version": "==2.0.4" + "markers": "python_version >= '3.8'", + "version": "==2.3.1" }, "pycodestyle": { "hashes": [ - "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", - "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" + "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", + "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521" ], - "markers": "python_version >= '3.6'", - "version": "==2.10.0" + "markers": "python_version >= '3.8'", + "version": "==2.12.1" }, "tomli": { "hashes": [ - "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", - "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], - "markers": "python_full_version < '3.11.0a7'", - "version": "==1.2.3" + "markers": "python_version < '3.11'", + "version": "==2.0.2" } } } diff --git a/README.md b/README.md index 42bdd92..cb466e5 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,39 @@ # Wachter Telegram bot -## Как запустить +[![Docker prod](https://github.com/alexeyqu/wachter_bot/actions/workflows/publish_production.yml/badge.svg)](https://github.com/alexeyqu/wachter_bot/actions/workflows/publish_production.yml) [![Docker testing](https://github.com/alexeyqu/wachter_bot/actions/workflows/publish_testing.yml/badge.svg)](https://github.com/alexeyqu/wachter_bot/actions/workflows/publish_testing.yml) -В корне есть Dockerfile +telegram logo [Вахтёр Бот](https://t.me/wachter_bot) + + +![photo_2023-10-28_15-36-58](https://github.com/alexeyqu/wachter_bot/assets/7394728/dac59c1b-0868-4bcc-aa07-48944c9a15b8) + + +## Как добавить в свою группу + +1. Добавить в группу. +2. Сделать администратором. +3. Опционально, настроить бота в личном чате. + +## Local Development + +### Prerequisites + +We are using black to keep our code looking nice and tidy. To make things easier, there's a pre-commit hook which ensures that files to commit are properly formatted. You need to install and initialize pre-commit. Any installation should suffice, one can find pipenv good choice since we use it in this project. After installation run: ```bash -$ docker build -t wachter_bot . -$ docker run -e TELEGRAM_TOKEN= wachter_bot +pre-commit install ``` -## Как добавить в свой чат +Then will be executed black against changed files in commits. -1. Добавить в чат -2. Написать whois (минимальная длина 20 символов) -3. Написать боту в лс /start - бот заработает и появятся настройки чата +### Running -## Local Development +1) Run `cp env.template .env`; -1) Set `TELEGRAM_TOKEN` environment variable; +1) Set `TELEGRAM_TOKEN` and `TELEGRAM_ERROR_CHAT_ID` in `.env`; 2) Run: ```bash -docker-compose -f docker-compose.dev.yml up -``` \ No newline at end of file +docker-compose -f docker-compose.dev.yml build && docker-compose -f docker-compose.dev.yml up +``` diff --git a/alembic.ini b/alembic.ini index 39a5bb2..6e2163b 100644 --- a/alembic.ini +++ b/alembic.ini @@ -35,8 +35,7 @@ script_location = migrations # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = postgresql://localhost:5432/wachter - +# sqlalchemy.url = postgresql+asyncpg://user:password@wachter-db/db # Logging configuration [loggers] diff --git a/app.py b/app.py new file mode 100644 index 0000000..387eb9c --- /dev/null +++ b/app.py @@ -0,0 +1,128 @@ +from telegram.ext import ( + ApplicationBuilder, + CommandHandler, + filters, + MessageHandler, + CallbackQueryHandler, + ChatMemberHandler, + PicklePersistence, +) +from ptbcontrib.ptb_jobstores.sqlalchemy import PTBSQLAlchemyJobStore + +import grpc +from opentelemetry import metrics +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.sdk import metrics as sdkmetrics +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import Resource + +from src.custom_filters import filter_bot_added +from src.logging import tg_logger +from src import handlers +import os + + +temporality_delta = { + sdkmetrics.Counter: AggregationTemporality.DELTA, + sdkmetrics.UpDownCounter: AggregationTemporality.DELTA, + sdkmetrics.Histogram: AggregationTemporality.DELTA, + sdkmetrics.ObservableCounter: AggregationTemporality.DELTA, + sdkmetrics.ObservableUpDownCounter: AggregationTemporality.DELTA, + sdkmetrics.ObservableGauge: AggregationTemporality.DELTA, +} + + +def main(): + dsn = os.environ.get("UPTRACE_DSN") + + exporter = OTLPMetricExporter( + endpoint="otlp.uptrace.dev:4317", + headers=(("uptrace-dsn", dsn),), + timeout=5, + compression=grpc.Compression.Gzip, + preferred_temporality=temporality_delta, + ) + reader = PeriodicExportingMetricReader(exporter) + + resource = Resource( + attributes={ + "service.name": "wachter", + "service.version": "1.1.0", + "deployment.environment": os.environ.get("DEPLOYMENT_ENVIRONMENT"), + } + ) + provider = MeterProvider(metric_readers=[reader], resource=resource) + metrics.set_meter_provider(provider) + + application = ( + ApplicationBuilder() + .persistence(PicklePersistence(filepath="persistent_storage.pickle")) + .token(os.environ["TELEGRAM_TOKEN"]) + .build() + ) + if "PERSISTENCE_DATABASE_URL" in os.environ: + tg_logger.info(f"Using SQLAlchemy job store with PERSISTENCE_DATABASE_URL") + application.job_queue.scheduler.add_jobstore( + PTBSQLAlchemyJobStore( + application=application, + url=os.environ["PERSISTENCE_DATABASE_URL"], + ) + ) + else: + tg_logger.info("No PERSISTENCE_DATABASE_URL set, using in-memory job store") + + application.add_handler(CommandHandler("help", handlers.help_handler)) + application.add_handler(CommandHandler("listjobs", handlers.list_jobs_handler)) + + # group UX + application.add_handler( + ChatMemberHandler( + handlers.my_chat_member_handler, + ChatMemberHandler.MY_CHAT_MEMBER, + ) + ) + application.add_handler( + MessageHandler( + filters.Entity("hashtag") & filters.ChatType.GROUPS, + handlers.on_hashtag_message, + ) + ) + application.add_handler( + MessageHandler( + filters.StatusUpdate.NEW_CHAT_MEMBERS & filter_bot_added, + handlers.on_new_chat_members, + ) + ) + + # admin UX + application.add_handler(CommandHandler("start", handlers.start_handler)) + application.add_handler(CallbackQueryHandler(handlers.button_handler)) + application.add_handler(MessageHandler(filters.TEXT, handlers.message_handler)) + application.add_error_handler(handlers.error_handler) + + # Remove any existing metrics_exporter jobs to avoid duplicates + existing_jobs = [job for job in application.job_queue.jobs() if job.name == "metrics_exporter"] + if existing_jobs: + tg_logger.info(f"Removing {len(existing_jobs)} existing metrics_exporter job(s)") + for job in existing_jobs: + job.schedule_removal() + + job = application.job_queue.run_repeating( + handlers.group.group_handler.db_metrics_reader_helper, + 3600, + name="metrics_exporter", + ) + tg_logger.info(f"Scheduled metrics_exporter job: {job.name}") + + tg_logger.info("Bot has started successfully") + application.run_polling() + + +if __name__ == "__main__": + main() diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f6b2274..4a84dea 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -22,6 +22,27 @@ services: timeout: 2s retries: 3 + wachter-persistence-db: + container_name: wachter-persistence-db-dev + image: postgres:alpine + restart: always + ports: + - 5434:5432 + environment: + POSTGRES_DB: db + POSTGRES_HOST: wachter-persistence-db + POSTGRES_USER: user + POSTGRES_PASSWORD: password + volumes: + - volume-persistence-db:/data/db + - postgres-persistence-data:/var/lib/postgresql/data + - ./share/sql-persistence:/docker-entrypoint-initdb.d + healthcheck: + test: pg_isready -U user -d db + interval: 5s + timeout: 2s + retries: 3 + wachter: container_name: wachter-dev build: @@ -29,7 +50,8 @@ services: environment: - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} - TELEGRAM_ERROR_CHAT_ID=${TELEGRAM_ERROR_CHAT_ID} - - DATABASE_URL=postgresql://user:password@wachter-db/db + - DATABASE_URL=postgresql+asyncpg://user:password@wachter-db/db + - PERSISTENCE_DATABASE_URL=postgresql://user:password@wachter-persistence-db/db restart: unless-stopped depends_on: wachter-db: @@ -38,6 +60,8 @@ services: volumes: volume-db: postgres-data: + volume-persistence-db: + postgres-persistence-data: networks: default: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2a762fc..40b1dfa 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,15 +1,66 @@ version: "3.9" services: - wachterbot: + wachterbot-prod: container_name: wachterbot-prod - image: ghcr.io/alexeyqu/wachter_bot/wachterbot:prod + image: ghcr.io/wachter-org/wachter-bot/wachterbot:prod environment: - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} + - UPTRACE_DSN=${UPTRACE_DSN} - TELEGRAM_ERROR_CHAT_ID=${TELEGRAM_ERROR_CHAT_ID} - - DATABASE_URL=${POSTGRES_URL} + - DATABASE_URL=${DATABASE_URL} + - PERSISTENCE_DATABASE_URL=${PERSISTENCE_DATABASE_URL} + - DEBUG=${DEBUG} + - DEPLOYMENT_ENVIRONMENT=production + - TEAM_TELEGRAM_IDS=${TEAM_TELEGRAM_IDS} restart: unless-stopped + postgres-prod: + image: postgres:17-alpine + restart: always + ports: + - 5435:5432 + environment: + POSTGRES_DB: db + POSTGRES_HOST: postgres-prod + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + volumes: + - volume-db:/data/db + - postgres-data:/var/lib/postgresql/data + - ./backup/db.sql:/docker-entrypoint-initdb.d/backupfile.sql + healthcheck: + test: pg_isready -U ${DATABASE_USER} -d db + interval: 5s + timeout: 2s + retries: 3 + + postgres-persistence-prod: + image: postgres:17-alpine + restart: always + ports: + - 5436:5432 + environment: + POSTGRES_DB: persistence-db + POSTGRES_HOST: postgres-persistence-prod + POSTGRES_USER: ${PERSISTENCE_DATABASE_USER} + POSTGRES_PASSWORD: ${PERSISTENCE_DATABASE_PASSWORD} + volumes: + - volume-persistence-db:/data/db + - postgres-persistence-data:/var/lib/postgresql/data + - ./backup/db-persistence.sql:/docker-entrypoint-initdb.d/backupfile.sql + healthcheck: + test: pg_isready -U ${PERSISTENCE_DATABASE_USER} -d persistence-db + interval: 5s + timeout: 2s + retries: 3 + +volumes: + volume-db: + postgres-data: + volume-persistence-db: + postgres-persistence-data: + networks: default: name: network-wachterbot-prod diff --git a/docker-compose.testing.yml b/docker-compose.testing.yml index 150a001..3f0701d 100644 --- a/docker-compose.testing.yml +++ b/docker-compose.testing.yml @@ -1,15 +1,66 @@ version: "3.9" services: - wachterbot: + wachterbot-testing: container_name: wachterbot-testing - image: ghcr.io/alexeyqu/wachter_bot/wachterbot:testing + image: ghcr.io/wachter-org/wachter-bot/wachterbot:testing environment: - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} - TELEGRAM_ERROR_CHAT_ID=${TELEGRAM_ERROR_CHAT_ID} - - DATABASE_URL=${POSTGRES_URL} + - DATABASE_URL=${DATABASE_URL} + - UPTRACE_DSN=${UPTRACE_DSN} + - PERSISTENCE_DATABASE_URL=${PERSISTENCE_DATABASE_URL} + - DEBUG=${DEBUG} + - DEPLOYMENT_ENVIRONMENT=testing + - TEAM_TELEGRAM_IDS=${TEAM_TELEGRAM_IDS} restart: unless-stopped + postgres-testing: + image: postgres:17-alpine + restart: always + ports: + - 5433:5432 + environment: + POSTGRES_DB: db + POSTGRES_HOST: postgres-testing + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + volumes: + - volume-db:/data/db + - postgres-data:/var/lib/postgresql/data + - ./backup/db.sql:/docker-entrypoint-initdb.d/backupfile.sql + healthcheck: + test: pg_isready -U ${DATABASE_USER} -d db + interval: 5s + timeout: 2s + retries: 3 + + postgres-persistence-testing: + image: postgres:17-alpine + restart: always + ports: + - 5434:5432 + environment: + POSTGRES_DB: persistence-db + POSTGRES_HOST: postgres-persistence-testing + POSTGRES_USER: ${PERSISTENCE_DATABASE_USER} + POSTGRES_PASSWORD: ${PERSISTENCE_DATABASE_PASSWORD} + volumes: + - volume-persistence-db:/data/db + - postgres-persistence-data:/var/lib/postgresql/data + - ./backup/db-persistence.sql:/docker-entrypoint-initdb.d/backupfile.sql + healthcheck: + test: pg_isready -U ${PERSISTENCE_DATABASE_USER} -d persistence-db + interval: 5s + timeout: 2s + retries: 3 + +volumes: + volume-db: + postgres-data: + volume-persistence-db: + postgres-persistence-data: + networks: default: name: network-wachterbot-testing diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..c3edb9b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +alembic upgrade head +exec python app.py diff --git a/.env b/env.template similarity index 100% rename from .env rename to env.template diff --git a/migrations/env.py b/migrations/env.py index 339eba7..bf1664e 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,12 +1,12 @@ -from __future__ import with_statement -from alembic import context -from sqlalchemy import create_engine, pool +import asyncio from logging.config import fileConfig +import os -import os, sys - -sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))) +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config +from alembic import context # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -14,27 +14,25 @@ # Interpret the config file for Python logging. # This line sets up loggers basically. -fileConfig(config.config_file_name) +if config.config_file_name is not None: + fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -from wachter import model - -target_metadata = model.Base.metadata +target_metadata = None # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. +config.set_main_option('sqlalchemy.url', os.environ.get( + "DATABASE_URL", "postgresql+asyncpg://user:password@wachter-db/db" +)) -def get_uri(): - return os.environ.get("DATABASE_URL", config.get_main_option("sqlalchemy.url")) - - -def run_migrations_offline(): +def run_migrations_offline() -> None: """Run migrations in 'offline' mode. This configures the context with just a URL @@ -46,28 +44,47 @@ def run_migrations_offline(): script output. """ + url = config.get_main_option("sqlalchemy.url") context.configure( - url=get_uri(), target_metadata=target_metadata, literal_binds=True + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations() -def run_migrations_online(): - """Run migrations in 'online' mode. +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) - In this scenario we need to create an Engine + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """In this scenario we need to create an Engine and associate a connection with the context. """ - connectable = create_engine(get_uri()) - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" - with context.begin_transaction(): - context.run_migrations() + asyncio.run(run_async_migrations()) if context.is_offline_mode(): diff --git a/migrations/versions/0336b796d052_create_tables_for_timeouts_and_whois_.py b/migrations/versions/0336b796d052_create_tables_for_timeouts_and_whois_.py new file mode 100644 index 0000000..5084dd3 --- /dev/null +++ b/migrations/versions/0336b796d052_create_tables_for_timeouts_and_whois_.py @@ -0,0 +1,38 @@ +"""create tables for timeouts and whois length + +Revision ID: e34af99a19b5 +Revises: 0336b796d052 +Create Date: 2023-10-21 16:56:05.421923 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "e34af99a19b5" +down_revision = "0336b796d052" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "chats", + sa.Column("notify_timeout", sa.Integer(), nullable=False, server_default="0"), + ) + op.add_column( + "chats", + sa.Column("whois_length", sa.Integer(), nullable=False, server_default="60"), + ) + + +def downgrade(): + op.drop_column( + "chats", + sa.Column("notify_timeout", sa.Integer(), nullable=False, server_default="0"), + ) + op.drop_column( + "chats", + sa.Column("whois_length", sa.Integer(), nullable=False, server_default="60"), + ) diff --git a/migrations/versions/296da7f6d724_remove_default_values.py b/migrations/versions/296da7f6d724_remove_default_values.py new file mode 100644 index 0000000..8d8672a --- /dev/null +++ b/migrations/versions/296da7f6d724_remove_default_values.py @@ -0,0 +1,92 @@ +"""remove default values + +Revision ID: 296da7f6d724 +Revises: 85798d8901da +Create Date: 2023-10-25 15:37:35.767976 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "296da7f6d724" +down_revision = "85798d8901da" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("chats", schema=None) as batch_op: + batch_op.alter_column( + "on_new_chat_member_message", existing_type=sa.TEXT(), server_default=None + ) + batch_op.alter_column( + "on_known_new_chat_member_message", + existing_type=sa.TEXT(), + server_default=None, + ) + batch_op.alter_column( + "on_introduce_message", existing_type=sa.TEXT(), server_default=None + ) + batch_op.alter_column( + "on_kick_message", existing_type=sa.TEXT(), server_default=None + ) + batch_op.alter_column( + "notify_message", existing_type=sa.TEXT(), server_default=None + ) + batch_op.alter_column( + "kick_timeout", existing_type=sa.INTEGER(), server_default=None + ) + batch_op.alter_column( + "notify_timeout", existing_type=sa.INTEGER(), server_default=None + ) + batch_op.alter_column( + "whois_length", existing_type=sa.INTEGER(), server_default=None + ) + batch_op.alter_column( + "on_introduce_message_update", existing_type=sa.TEXT(), server_default=None + ) + + +def downgrade(): + with op.batch_alter_table("chats", schema=None) as batch_op: + batch_op.alter_column( + "on_new_chat_member_message", + existing_type=sa.TEXT(), + server_default="Пожалуйста, представьтесь и поздоровайтесь с сообществом.", + ) + batch_op.alter_column( + "on_known_new_chat_member_message", + existing_type=sa.TEXT(), + server_default="Добро пожаловать. Снова", + ) + batch_op.alter_column( + "on_introduce_message", + existing_type=sa.TEXT(), + server_default="Добро пожаловать.", + ) + batch_op.alter_column( + "on_kick_message", + existing_type=sa.TEXT(), + server_default="%USER\_MENTION% молчит и покидает чат", + ) + batch_op.alter_column( + "notify_message", + existing_type=sa.TEXT(), + server_default="%USER\_MENTION%, пожалуйста, представьтесь и поздоровайтесь с сообществом.", + ) + batch_op.alter_column( + "kick_timeout", existing_type=sa.INTEGER(), server_default="0" + ) + batch_op.alter_column( + "notify_timeout", existing_type=sa.INTEGER(), server_default="0" + ) + batch_op.alter_column( + "whois_length", existing_type=sa.INTEGER(), server_default="60" + ) + batch_op.alter_column( + "on_introduce_message_update", + existing_type=sa.TEXT(), + server_default="Если вы хотите обновить, то добавьте тег #update к сообщению.", + ) diff --git a/migrations/versions/85798d8901da_add_on_introduce_message_update_column.py b/migrations/versions/85798d8901da_add_on_introduce_message_update_column.py new file mode 100644 index 0000000..43aa980 --- /dev/null +++ b/migrations/versions/85798d8901da_add_on_introduce_message_update_column.py @@ -0,0 +1,40 @@ +"""add on_introduce_message_update column + +Revision ID: 85798d8901da +Revises: e34af99a19b5 +Create Date: 2023-10-23 22:48:45.471633 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "85798d8901da" +down_revision = "e34af99a19b5" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "chats", + sa.Column( + "on_introduce_message_update", + sa.Text(), + nullable=False, + server_default="Если вы хотите обновить, то добавьте тег #update к сообщению.", + ), + ) + + +def downgrade(): + op.drop_column( + "chats", + sa.Column( + "on_introduce_message_update", + sa.Text(), + nullable=False, + server_default="Если вы хотите обновить, то добавьте тег #update к сообщению.", + ), + ) diff --git a/wachter/__init__.py b/src/__init__.py similarity index 100% rename from wachter/__init__.py rename to src/__init__.py diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..649f8d7 --- /dev/null +++ b/src/constants.py @@ -0,0 +1,39 @@ +from enum import IntEnum, auto +import json, os + + +default_kick_timeout_m = 1440 # 24h in minutes +default_notify_timeout_m = 1380 # 23h in minutes +default_delete_message_timeout_m = 60 # 1h in minutes +default_whois_length = 60 + + +# ACTIONS +class Actions(IntEnum): + start_select_chat = auto() + select_chat = auto() + set_on_new_chat_member_message_response = auto() + set_notify_message = auto() + set_on_successful_introducion_response = auto() + set_on_known_new_chat_member_message_response = auto() + set_kick_timeout = auto() + set_on_kick_message = auto() + get_current_settings = auto() + set_intro_settings = auto() + set_kick_bans_settings = auto() + back_to_chats = auto() + set_notify_timeout = auto() + get_current_kick_settings = auto() + get_current_intro_settings = auto() + set_whois_length = auto() + set_on_introduce_message_update = auto() + + +DEBUG = os.environ.get("DEBUG", "True") in ["True"] +TEAM_TELEGRAM_IDS = json.loads(os.environ.get("TEAM_TELEGRAM_IDS", "[]")) + + +def get_uri(): + return os.environ.get( + "DATABASE_URL", "postgresql+asyncpg://user:password@wachter-db/db" + ) diff --git a/wachter/custom_filters.py b/src/custom_filters.py similarity index 54% rename from wachter/custom_filters.py rename to src/custom_filters.py index 7910a50..9ef68a3 100644 --- a/wachter/custom_filters.py +++ b/src/custom_filters.py @@ -1,7 +1,9 @@ -from telegram.ext import BaseFilter +from telegram.ext import filters -class FilterBotAdded(BaseFilter): # filter for message, that bot was added to group +class FilterBotAdded( + filters.MessageFilter +): # filter for message, that bot was added to group def filter(self, message): if message.new_chat_members[-1].is_bot: return False diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py new file mode 100644 index 0000000..ed0441d --- /dev/null +++ b/src/handlers/__init__.py @@ -0,0 +1,10 @@ +""" +Module with all the telegram handlers. +""" + +from .help_handler import help_handler +from .error_handler import error_handler + +from .admin import * +from .debug import * +from .group import * diff --git a/src/handlers/admin/__init__.py b/src/handlers/admin/__init__.py new file mode 100644 index 0000000..42d0408 --- /dev/null +++ b/src/handlers/admin/__init__.py @@ -0,0 +1,6 @@ +""" +Module with all the telegram handlers related to admin flow. +""" + +from .start_handler import start_handler +from .menu_handler import button_handler, message_handler diff --git a/src/handlers/admin/menu_handler.py b/src/handlers/admin/menu_handler.py new file mode 100644 index 0000000..c27c55a --- /dev/null +++ b/src/handlers/admin/menu_handler.py @@ -0,0 +1,574 @@ +import json +from telegram import InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode +from datetime import datetime, timedelta +from typing import Callable, Optional + +from sqlalchemy import select + +from src import constants +from src.model import Chat, session_scope +from src.texts import _ + +from src.handlers.group.group_handler import on_kick_timeout, on_notify_timeout + +from .utils import ( + get_chats_list, + create_chats_list_keyboard, + new_button, + new_keyboard_layout, + get_chat_name, +) + +from src.logging import tg_logger + + +async def _job_rescheduling_helper( + job_func: Callable, timeout: int, context: ContextTypes.DEFAULT_TYPE, chat_id: int +) -> None: + """ + This function helps in rescheduling a job in the Telegram bot's job queue. + + Args: + job_func (Callable): The function that is to be scheduled as a job. This is the callback function that is executed when the job runs. + timeout (int): The amount of time (in minutes) after which the job should be executed. + context (ContextTypes.DEFAULT_TYPE): The callback context as provided by the Telegram API. This provides a context containing information about the current state of the bot and the update it is handling. + chat_id (int): The unique identifier for the chat. This is used to query the database for chat-specific settings. + + Returns: + None: This function does not return anything. + """ + # Iterating through all the jobs currently in the job queue + for job in context.job_queue.jobs(): + # If the job's name matches the name of the job function provided + if job.name == job_func.__name__ and chat_id == job.data.get('chat_id'): + # Extracting the job context and calculating the new timeout + job_data = job.data + job_creation_time = datetime.fromtimestamp(job_data.get("creation_time")) + new_timeout = job_creation_time + timedelta(seconds=timeout * 60) + + # If the new timeout is in the past, set it to now + if new_timeout < datetime.now(): + new_timeout = datetime.now() + + # Schedule the current job for removal + job.schedule_removal() + + # If the job is a notification timeout, perform additional checks + if job_func == on_notify_timeout: + # Querying the database to get the chat's kick timeout setting + async with session_scope() as sess: + result = await sess.execute(select(Chat).filter(Chat.id == chat_id)) + chat: Optional[Chat] = result.scalars().first() + kick_timeout = chat.kick_timeout if chat else 0 + + # If the new timeout is greater than the kick timeout, skip to the next job + if ( + job_creation_time + timedelta(seconds=kick_timeout * 60) + ) > new_timeout: + continue + + # Update the job context with the new timeout + job_data["timeout"] = new_timeout + + # Schedule the new job with the updated context and timeout + job = context.job_queue.run_once(job_func, new_timeout, data=job_data) + + +async def _get_current_settings_helper( + chat_id: int, settings: str, chat_name: str +) -> str: + """ + Retrieve the current settings for a specific chat based on the settings category provided. + This function is now an asynchronous function. + + Args: + chat_id (int): The ID of the chat for which the settings are to be retrieved. + settings (str): A string indicating the category of settings to retrieve. + + Returns: + str: A formatted message string containing the current settings. + """ + async with session_scope() as session: + result = await session.execute(select(Chat).filter(Chat.id == chat_id)) + chat: Optional[Chat] = result.scalars().first() + + if settings == constants.Actions.get_current_intro_settings: + print("Loading intro settings for chat_id:", chat_id) + return ( + _("msg__get_intro_settings") + .format(chat_name=chat_name, **chat.__dict__) + .replace("%USER\\_MENTION%", "%USER_MENTION%") + ) + else: + return ( + _("msg__get_kick_settings") + .format(chat_name=chat_name, **chat.__dict__) + .replace("%USER\\_MENTION%", "%USER_MENTION%") + ) + + +# todo rework into callback folder +async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Handle button presses in the Telegram bot's inline keyboard. + + Args: + update (Update): The Telegram update object. + context (ContextTypes.DEFAULT_TYPE): The callback context as provided by the Telegram API. + + Returns: + None + """ + query = update.callback_query + data = json.loads(query.data) + + if data["action"] == constants.Actions.start_select_chat: + user_id = query.from_user.id + user_chats = await get_chats_list(user_id, context) + if len(user_chats) == 0: + await update.message.reply_text(_("msg__no_chats_available")) + return + reply_markup = InlineKeyboardMarkup( + await create_chats_list_keyboard(user_chats, context, user_id) + ) + await context.bot.edit_message_text( + _("msg__start_command"), + reply_markup=reply_markup, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + + if data["action"] == constants.Actions.select_chat: + selected_chat_id = data["chat_id"] + button_configs = [ + [{"text": _("btn__intro"), "action": constants.Actions.set_intro_settings}], + [ + { + "text": _("btn__kicks"), + "action": constants.Actions.set_kick_bans_settings, + } + ], + [ + { + "text": _("btn__back_to_chats"), + "action": constants.Actions.back_to_chats, + } + ], + ] + reply_markup = new_keyboard_layout(button_configs, selected_chat_id) + chat_name = await get_chat_name(context.bot, selected_chat_id) + await context.bot.edit_message_text( + _("msg__select_chat").format(chat_name=chat_name), + reply_markup=reply_markup, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + elif data["action"] == constants.Actions.set_intro_settings: + selected_chat_id = data["chat_id"] + button_configs = [ + [ + { + "text": _("btn__current_settings"), + "action": constants.Actions.get_current_intro_settings, + } + ], + [ + { + "text": _("btn__change_welcome_message"), + "action": constants.Actions.set_on_new_chat_member_message_response, + } + ], + [ + { + "text": _("btn__change_rewelcome_message"), + "action": constants.Actions.set_on_known_new_chat_member_message_response, + } + ], + [ + { + "text": _("btn__change_notify_message"), + "action": constants.Actions.set_notify_message, + } + ], + [ + { + "text": _("btn__change_sucess_message"), + "action": constants.Actions.set_on_successful_introducion_response, + } + ], + [ + { + "text": _("btn__change_notify_timeout"), + "action": constants.Actions.set_notify_timeout, + } + ], + [ + { + "text": _("btn__change_whois_length"), + "action": constants.Actions.set_whois_length, + } + ], + [ + { + "text": _("btn__change_whois_message"), + "action": constants.Actions.set_on_introduce_message_update, + } + ], + [{"text": _("btn__back"), "action": constants.Actions.select_chat}], + ] + reply_markup = new_keyboard_layout(button_configs, selected_chat_id) + await context.bot.edit_message_reply_markup( + reply_markup=reply_markup, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + + elif data["action"] == constants.Actions.set_kick_bans_settings: + selected_chat_id = data["chat_id"] + button_configs = [ + [ + { + "text": _("btn__current_settings"), + "action": constants.Actions.get_current_kick_settings, + } + ], + [ + { + "text": _("btn__change_kick_timeout"), + "action": constants.Actions.set_kick_timeout, + } + ], + [ + { + "text": _("btn__change_kick_message"), + "action": constants.Actions.set_on_kick_message, + } + ], + [{"text": _("btn__back"), "action": constants.Actions.select_chat}], + ] + reply_markup = new_keyboard_layout(button_configs, selected_chat_id) + await context.bot.edit_message_reply_markup( + reply_markup=reply_markup, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + + elif data["action"] == constants.Actions.back_to_chats: + user_id = query.message.chat_id + user_chats = await get_chats_list(user_id, context) + reply_markup = InlineKeyboardMarkup( + await create_chats_list_keyboard(user_chats, context, user_id) + ) + await context.bot.edit_message_text( + _("msg__start_command"), + reply_markup=reply_markup, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + + elif data["action"] == constants.Actions.set_on_new_chat_member_message_response: + await context.bot.edit_message_text( + text=_("msg__set_new_welcome_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_kick_timeout: + await context.bot.edit_message_text( + text=_("msg__set_new_kick_timout"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif ( + data["action"] + == constants.Actions.set_on_known_new_chat_member_message_response + ): + await context.bot.edit_message_text( + text=_("msg__set_new_rewelcome_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_notify_message: + await context.bot.edit_message_text( + text=_("msg__set_new_notify_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_on_new_chat_member_message_response: + await context.bot.edit_message_text( + text=_("msg__set_new_welcome_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_on_successful_introducion_response: + await context.bot.edit_message_text( + text=_("msg__set_new_sucess_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_whois_length: + await context.bot.edit_message_text( + text=_("msg__set_new_whois_length"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_on_kick_message: + await context.bot.edit_message_text( + text=_("msg__set_new_kick_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_notify_timeout: + await context.bot.edit_message_text( + text=_("msg__set_new_notify_timeout"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.set_on_introduce_message_update: + await context.bot.edit_message_text( + text=_("msg__set_new_whois_message"), + chat_id=query.message.chat_id, + message_id=query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + context.user_data["chat_id"] = data["chat_id"] + context.user_data["action"] = data["action"] + + elif data["action"] == constants.Actions.get_current_intro_settings: + keyboard = [ + [ + new_button( + _("btn__back"), + data["chat_id"], + constants.Actions.set_intro_settings, + ) + ] + ] + chat_name = await get_chat_name(context.bot, data["chat_id"]) + reply_markup = InlineKeyboardMarkup(keyboard) + settings = await _get_current_settings_helper( + data["chat_id"], data["action"], chat_name + ) + await context.bot.edit_message_text( + text=settings, + parse_mode=ParseMode.MARKDOWN, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + reply_markup=reply_markup, + ) + + context.user_data["action"] = None + + elif data["action"] == constants.Actions.get_current_kick_settings: + keyboard = [ + [ + new_button( + _("btn__back"), + data["chat_id"], + constants.Actions.set_kick_bans_settings, + ) + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + chat_name = await get_chat_name(context.bot, data["chat_id"]) + settings = await _get_current_settings_helper( + data["chat_id"], data["action"], chat_name + ) + await context.bot.edit_message_text( + text=settings, + parse_mode=ParseMode.MARKDOWN, + chat_id=query.message.chat_id, + message_id=query.message.message_id, + reply_markup=reply_markup, + ) + + context.user_data["action"] = None + + +async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Handle text messages received by the Telegram bot. + + Args: + update (Update): The Telegram update object. + context (ContextTypes.DEFAULT_TYPE): The callback context as provided by the Telegram API. + + Returns: + None: This function returns nothing. + """ + chat_id = update.effective_message.chat_id + + if chat_id > 0: + action = context.user_data.get("action") + + if action is None: + return + + chat_id = context.user_data["chat_id"] + + if action == constants.Actions.set_kick_timeout: + message = update.effective_message.text + try: + timeout = int(message) + assert timeout >= 0 + except: + await update.effective_message.reply_text( + _("msg__failed_set_kick_timeout_response") + ) + return + async with session_scope() as sess: + chat = Chat(id=chat_id, kick_timeout=timeout) + await sess.merge(chat) + context.user_data["action"] = None + await _job_rescheduling_helper(on_kick_timeout, timeout, context, chat_id) + + keyboard = [ + [ + new_button( + _("btn__back"), + chat_id, + constants.Actions.set_kick_bans_settings, + ) + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.effective_message.reply_text( + _("msg__success_set_kick_timeout_response"), + reply_markup=reply_markup, + ) + + elif action == constants.Actions.set_notify_timeout: + message = update.effective_message.text + try: + timeout = int(message) + assert timeout >= 0 + except: + await update.effective_message.reply_text(_("msg__failed_kick_response")) + return + async with session_scope() as sess: + chat = Chat(id=chat_id, notify_timeout=timeout) + await sess.merge(chat) + context.user_data["action"] = None + await _job_rescheduling_helper(on_notify_timeout, timeout, context, chat_id) + + keyboard = [ + [ + new_button( + _("btn__back"), chat_id, constants.Actions.set_intro_settings + ) + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.effective_message.reply_text( + _("msg__sucess_set_notify_timeout_response"), + reply_markup=reply_markup, + ) + + elif action in [ + constants.Actions.set_on_new_chat_member_message_response, + constants.Actions.set_notify_message, + constants.Actions.set_on_known_new_chat_member_message_response, + constants.Actions.set_on_successful_introducion_response, + constants.Actions.set_on_kick_message, + constants.Actions.set_whois_length, + constants.Actions.set_on_introduce_message_update, + ]: + message = update.effective_message.text_markdown + reply_message = _("msg__set_new_message") + async with session_scope() as sess: + if action == constants.Actions.set_on_new_chat_member_message_response: + chat = Chat(id=chat_id, on_new_chat_member_message=message) + if ( + action + == constants.Actions.set_on_known_new_chat_member_message_response + ): + chat = Chat(id=chat_id, on_known_new_chat_member_message=message) + if action == constants.Actions.set_on_successful_introducion_response: + chat = Chat(id=chat_id, on_introduce_message=message) + if action == constants.Actions.set_notify_message: + chat = Chat(id=chat_id, notify_message=message) + if action == constants.Actions.set_on_kick_message: + chat = Chat(id=chat_id, on_kick_message=message) + if action == constants.Actions.set_whois_length: + try: + whois_length = int(message) + assert whois_length >= 0 + chat = Chat(id=chat_id, whois_length=whois_length) + reply_message = _("msg__sucess_whois_length") + except: + await update.effective_message.reply_text(_("msg__failed_whois_response")) + return + + if action == constants.Actions.set_on_introduce_message_update: + if ( + "#update" + not in update.effective_message.parse_entities(types=["hashtag"]).values() + ): + await update.effective_message.reply_text( + _("msg__need_hashtag_update_response") + ) + return + chat = Chat(id=chat_id, on_introduce_message_update=message) + await sess.merge(chat) + + if action in [ + constants.Actions.set_on_kick_message, + constants.Actions.set_kick_timeout, + ]: + keyboard = [ + [ + new_button( + _("btn__back"), + chat_id, + constants.Actions.set_kick_bans_settings, + ) + ] + ] + else: + keyboard = [ + [ + new_button( + _("btn__back"), + chat_id, + constants.Actions.set_intro_settings, + ) + ] + ] + context.user_data["action"] = None + + reply_markup = InlineKeyboardMarkup(keyboard) + await update.effective_message.reply_text(reply_message, reply_markup=reply_markup) diff --git a/src/handlers/admin/start_handler.py b/src/handlers/admin/start_handler.py new file mode 100644 index 0000000..f4c6abd --- /dev/null +++ b/src/handlers/admin/start_handler.py @@ -0,0 +1,41 @@ +from telegram import InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes + + +from src.handlers.utils import admin +from src.texts import _ + + +from .utils import get_chats_list, create_chats_list_keyboard + + +@admin +async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Handle the /start command in a Telegram chat. + + Args: + update (Update): The update object that represents the incoming update. + context (ContextTypes.DEFAULT_TYPE): The context object that contains information about the current state of the bot. + + Returns: + None + """ + # Get the ID of the user who sent the message + user_id = update.message.chat_id + + # Retrieve the list of chats where the user has administrative privileges + user_chats = await get_chats_list(user_id, context) + + # If the user does not have administrative privileges in any chat, inform them + if len(user_chats) == 0: + await update.message.reply_text(_("msg__no_chats_available")) + return + + # Create an inline keyboard with the list of available chats + reply_markup = InlineKeyboardMarkup( + await create_chats_list_keyboard(user_chats, context, user_id) + ) + + # Send a message to the user with the inline keyboard + await update.message.reply_text(_("msg__start_command"), reply_markup=reply_markup) diff --git a/src/handlers/admin/utils.py b/src/handlers/admin/utils.py new file mode 100644 index 0000000..0437826 --- /dev/null +++ b/src/handlers/admin/utils.py @@ -0,0 +1,132 @@ +import json +import time +from typing import Iterator, Dict, List + +from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import CallbackContext + +from sqlalchemy import select + +from src.logging import tg_logger +from src.model import User, session_scope +from src import constants + + +async def get_chat_name(bot, chat_id): + chat = await bot.get_chat(chat_id) + return chat.title or str(chat_id) + + +def new_button(text: str, chat_id: int, action) -> InlineKeyboardButton: + """ + Create a new InlineKeyboardButton with associated callback data. + + Args: + text (str): The text to be displayed on the button. + chat_id (int): The chat ID to be included in the callback data. + action (str): The action to be performed, included in the callback data. + + Returns: + InlineKeyboardButton: The created InlineKeyboardButton instance. + """ + callback_data = json.dumps({"chat_id": chat_id, "action": action}) + return InlineKeyboardButton(text, callback_data=callback_data) + + +def new_keyboard_layout( + button_configs: List[List[Dict[str, str]]], chat_id: int +) -> InlineKeyboardMarkup: + """ + Create a new InlineKeyboardMarkup layout based on a configuration list. + + Args: + button_configs (List[List[Dict[str, str]]]): A list of button configurations. + chat_id (int): The chat ID to be included in the callback data of each button. + + Returns: + InlineKeyboardMarkup: The created InlineKeyboardMarkup instance. + """ + keyboard = [ + [new_button(button["text"], chat_id, button["action"]) for button in row] + for row in button_configs + ] + return InlineKeyboardMarkup(keyboard) + + +async def authorize_user(bot: Bot, chat_id: int, user_id: int) -> bool: + """ + Asynchronously check if a user is an administrator or the creator of a chat. + + Args: + bot (Bot): The Telegram Bot instance. + chat_id (int): The ID of the chat. + user_id (int): The ID of the user. + + Returns: + bool: True if the user is an administrator or creator of the chat, False otherwise. + """ + try: + chat_member = await bot.get_chat_member(chat_id, user_id) + return chat_member.status in ["creator", "administrator"] + except Exception as e: + print(f"Failed to check if user {user_id} is admin in chat {chat_id}: {e}") + return False + + +async def get_chats_list( + user_id: int, context: CallbackContext +) -> List[Dict[str, int]]: + """ + Retrieve a list of chats where the user is an administrator or creator. + + This function queries the database for User instances associated with the provided + user_id. For each User instance found, it checks whether the provided user_id + is an authorized user of the associated chat. If so, the function retrieves the + chat's title and id, and adds them to a list which is returned after all + authorized chats have been processed. + + Args: + user_id (int): The ID of the user. + context (CallbackContext): The callback context as provided by the Telegram Bot API. + + Returns: + List[Dict[str, int]]: A list of dictionaries, each containing the 'title' and 'id' + of a chat where the user has administrative or creator rights. + """ + time_start = time.time() + async with session_scope() as session: # Ensure this yields an AsyncSession object. + result = await session.execute(select(User).filter(User.user_id == user_id)) + users = result.scalars().all() + chats_list = [] + for user in users: + try: + if await authorize_user(context.bot, user.chat_id, user_id): + chat_name = await get_chat_name(context.bot, user.chat_id) + chats_list.append({"title": chat_name, "id": user.chat_id}) + except Exception as e: + context.bot.logger.exception( + e + ) # Ensure your CallbackContext has a logger configured. + tg_logger.info(f'get_chats_list time elapsed {time.time() - time_start}s') + return chats_list + + +async def create_chats_list_keyboard( + user_chats: Iterator[Dict[str, int]], context: CallbackContext, user_id: int +) -> List[List[InlineKeyboardButton]]: + """ + Create a keyboard layout for the list of chats where the user is an administrator or creator. + + Args: + user_chats (Iterator[Dict[str, int]]): An iterator over dictionaries containing chat information. + context (CallbackContext): The callback context as provided by the Telegram API. + user_id (int): The ID of the user. + + Returns: + List[List[InlineKeyboardButton]]: The created keyboard layout. + """ + return [ + [new_button(chat["title"], chat["id"], constants.Actions.select_chat)] + for chat in user_chats + if await authorize_user(context.bot, chat["id"], user_id) + ] diff --git a/src/handlers/debug/__init__.py b/src/handlers/debug/__init__.py new file mode 100644 index 0000000..fbf7a5d --- /dev/null +++ b/src/handlers/debug/__init__.py @@ -0,0 +1,5 @@ +""" +Module with all the telegram handlers used for debugging. +""" + +from .list_jobs_handler import list_jobs_handler diff --git a/src/handlers/debug/list_jobs_handler.py b/src/handlers/debug/list_jobs_handler.py new file mode 100644 index 0000000..bf14e99 --- /dev/null +++ b/src/handlers/debug/list_jobs_handler.py @@ -0,0 +1,24 @@ +import html +from telegram import Update +from telegram.constants import ParseMode +from telegram.ext import CallbackContext + +from src import constants +from src.handlers.utils import debug + + +@debug +async def list_jobs_handler(update: Update, context: CallbackContext) -> None: + args = update.effective_message.text.split() + chat_id = int(args[1]) if len(args) > 1 else None + jobs = [job for job in context.job_queue.jobs() if chat_id is None or job.data.get("chat_id") == chat_id] + await update.message.reply_text( + f"Jobs: {len(jobs)} items\n\n" + + "\n\n".join( + [ + f"Job {html.escape(job.name)} ts {html.escape(str(job.next_t)) if job.next_t else 'None'}\nContext: {html.escape(str(job.data))}" + for job in jobs + ] + ), + parse_mode=ParseMode.HTML, + ) diff --git a/src/handlers/error_handler.py b/src/handlers/error_handler.py new file mode 100644 index 0000000..db75461 --- /dev/null +++ b/src/handlers/error_handler.py @@ -0,0 +1,66 @@ +from telegram import Update +from telegram.ext import CallbackContext +from telegram.constants import ParseMode +import logging +import traceback +import os +import html +import json +from src.logging import tg_logger + +logger = logging.getLogger(__name__) + +async def error_handler(update: Update, context: CallbackContext): + """ + Log the error and send a telegram message to notify the developer. + Credits: https://docs.python-telegram-bot.org/en/stable/examples.errorhandlerbot.html + """ + try: + # Safety check: ensure context and error exist + if not context or not context.error: + logger.error("Error handler called but context or context.error is None") + return + + # Log the error before we do anything else, so we can see it even if something breaks. + logger.error("Exception while handling an update:", exc_info=context.error) + + # traceback.format_exception returns the usual python message about an exception, but as a + # list of strings rather than a single string, so we have to join them together. + tb_list = traceback.format_exception(None, context.error, context.error.__traceback__) + tb_string = "".join(tb_list) + + # Build the message with some markup and additional information about what happened. + # You might need to add some logic to deal with messages longer than the 4096 character limit. + update_str = update.to_dict() if isinstance(update, Update) and update else str(update) + message = ( + "An exception was raised while handling an update\n" + f"
update = {html.escape(json.dumps(update_str, indent=2, ensure_ascii=False))}"
+            "
\n\n" + f"
context.chat_data = {html.escape(str(context.chat_data))}
\n\n" + f"
context.user_data = {html.escape(str(context.user_data))}
\n\n" + f"
{html.escape(tb_string)}
" + ) + + # Truncate message if it's too long (Telegram has a 4096 character limit) + if len(message) > 4096: + message = message[:4000] + "\n\n... (message truncated)" + + # Finally, send the message (only if TELEGRAM_ERROR_CHAT_ID is set and bot is available) + error_chat_id = os.environ.get("TELEGRAM_ERROR_CHAT_ID") + if error_chat_id and context.bot: + try: + await context.bot.send_message( + chat_id=error_chat_id, text=message, parse_mode=ParseMode.HTML + ) + except Exception as send_error: + # If sending the error message fails, log it but don't raise + logger.error(f"Failed to send error message to Telegram: {send_error}", exc_info=send_error) + except Exception as handler_error: + # If the error handler itself fails, log it but don't raise to avoid infinite recursion + logger.critical(f"Error handler itself raised an exception: {handler_error}", exc_info=handler_error) + # Fallback to simple print if logger also fails + try: + print(f"CRITICAL: Error handler failed: {handler_error}") + print(f"Original error: {context.error if context and context.error else 'Unknown'}") + except Exception: + pass # Last resort - if even print fails, we're in deep trouble \ No newline at end of file diff --git a/src/handlers/group/__init__.py b/src/handlers/group/__init__.py new file mode 100644 index 0000000..d2e3dba --- /dev/null +++ b/src/handlers/group/__init__.py @@ -0,0 +1,6 @@ +""" +Module with all the telegram handlers related to group chat flow. +""" + +from .group_handler import on_hashtag_message, on_new_chat_members +from .my_chat_member_handler import my_chat_member_handler diff --git a/src/handlers/group/group_handler.py b/src/handlers/group/group_handler.py new file mode 100644 index 0000000..9e9da5c --- /dev/null +++ b/src/handlers/group/group_handler.py @@ -0,0 +1,390 @@ +from datetime import datetime, timedelta +from telegram import Bot, Message, Update +from telegram.constants import ParseMode +from telegram.ext import ContextTypes +from typing import Optional + +from sqlalchemy import select, func + +from src.logging import tg_logger +from src import constants +from src.texts import _ +from src.model import Chat, User, session_scope +from src.handlers.utils import setup_counter, setup_histogram + + +new_member_counter = setup_counter("new_member.meter", "new_member_counter") +whois_counter = setup_counter("new_whois.meter", "new_whois_counter") +ban_counter = setup_counter("ban.meter", "ban_counter") +chats_histogram = setup_histogram("chats.meter", "chats_counter") +users_histogram = setup_histogram("users.meter", "users_counter") +unique_users_histogram = setup_histogram( + "unique_users.meter", "unique_users_counter" +) + +async def db_metrics_reader_helper(context: ContextTypes.DEFAULT_TYPE): + async with session_scope() as sess: + # Number of chats + result = await sess.execute(select(func.count(Chat.id))) + chat_count = result.scalar() + chats_histogram.record(chat_count) + # Total number of users + result = await sess.execute(select(func.count()).select_from(User)) + users_count = result.scalar() + users_histogram.record(users_count) + # Number of unique users + result = await sess.execute(select(func.count(func.distinct(User.user_id)))) + unique_users_count = result.scalar() + unique_users_histogram.record(unique_users_count) + + +async def on_new_chat_members( + update: Update, context: ContextTypes.DEFAULT_TYPE +) -> None: + """ + Handle the event when a new member joins a chat. + + Args: + update (Update): The update object that represents the incoming update. + context (CallbackContext): The context object that contains information about the current state of the bot. + + Returns: + None + """ + chat_id = update.message.chat_id + new_member_counter.add(1, {"chat_id": chat_id}) + user_ids = [ + new_chat_member.id for new_chat_member in update.message.new_chat_members + ] + + for user_id in user_ids: + for job in context.job_queue.jobs(): + if ( + job.data + and job.data.get("user_id") == user_id + and job.data.get("chat_id") == chat_id + ): + job.schedule_removal() + + async with session_scope() as sess: + result = await sess.execute( + select(User).where(User.chat_id == chat_id, User.user_id == user_id) + ) + user = result.scalars().first() + chat_result = await sess.execute(select(Chat).where(Chat.id == chat_id)) + chat = chat_result.scalars().first() + + if chat is None: + chat = Chat.get_new_chat(chat_id) + sess.add(chat) + await sess.commit() + + if user is not None: + await _send_message_with_deletion( + context, + chat_id, + user_id, + chat.on_known_new_chat_member_message, + reply_to=update.message, + ) + continue + + message = chat.on_new_chat_member_message + kick_timeout = chat.kick_timeout + notify_timeout = chat.notify_timeout + + if message == _("msg__skip_new_chat_member"): + continue + + await _send_message_with_deletion( + context, + chat_id, + user_id, + message, + # 36 hours which is considered infinity; bots can't delete messages older than 48h + timeout_m=constants.default_delete_message_timeout_m * 24 * 1.5, + reply_to=update.message, + ) + + if kick_timeout != 0: + job = context.job_queue.run_once( + on_kick_timeout, + kick_timeout * 60, + chat_id=chat_id, + user_id=user_id, + data={ + "chat_id": chat_id, + "user_id": user_id, + "creation_time": datetime.now().timestamp(), + }, + ) + + if notify_timeout != 0: + job = context.job_queue.run_once( + on_notify_timeout, + notify_timeout * 60, + chat_id=chat_id, + user_id=user_id, + data={ + "chat_id": chat_id, + "user_id": user_id, + "creation_time": datetime.now().timestamp(), + }, + ) + + +def is_whois(update, chat_id): + return ( + "#whois" in update.effective_message.parse_entities(types=["hashtag"]).values() + and chat_id < 0 + ) + + +async def remove_user_jobs_from_queue(context, user_id, chat_id): + """ + Remove jobs related to a specific user from the job queue. + + Args: + context (CallbackContext): The context object containing the job queue and bot instance. + user_id (int): The user ID for whom the jobs should be removed. + chat_id (int): The chat ID associated with the jobs to be removed. + + Returns: + bool: True if at least one job was removed, False otherwise. + """ + removed = False + for job in context.job_queue.jobs(): + if job.data and job.data.get("user_id") == user_id and job.data.get("chat_id") == chat_id: + if "message_id" in job.data: + try: + await context.bot.delete_message( + job.data.get("chat_id"), job.data["message_id"] + ) + except Exception as e: + tg_logger.warning( + f"can't delete {job.data['message_id']} from {job.data['chat_id']}", + exc_info=e, + ) + job.schedule_removal() + removed = True + return removed + + +async def on_hashtag_message( + update: Update, context: ContextTypes.DEFAULT_TYPE +) -> None: + """ + Handle messages containing #whois hashtag. + + Args: + update (Update): The update object that represents the incoming update. + context (CallbackContext): The context object that contains information about the current state of the bot. + + Returns: + None + """ + chat_id = update.effective_message.chat_id + + if is_whois(update, chat_id): + user_id = update.effective_message.from_user.id + whois_counter.add(1) + + async with session_scope() as sess: + chat_result = await sess.execute(select(Chat).where(Chat.id == chat_id)) + chat = chat_result.scalars().first() + if chat is None: + chat = Chat.get_new_chat(chat_id) + sess.add(chat) + await sess.commit() + + if len(update.effective_message.text) <= chat.whois_length: + await _send_message_with_deletion( + context, + chat_id, + user_id, + # TODO move to chat DB + _("msg__short_whois").format(whois_length=chat.whois_length), + reply_to=update.effective_message, + ) + return + + message = chat.on_introduce_message + + async with session_scope() as sess: + user = User( + chat_id=chat_id, user_id=user_id, whois=update.effective_message.text + ) + await sess.merge(user) + + removed = False + removed = await remove_user_jobs_from_queue(context, user_id, chat_id) + + if removed: + await _send_message_with_deletion( + context, + chat_id, + user_id, + message, + reply_to=update.effective_message, + ) + + +async def on_notify_timeout(context: ContextTypes.DEFAULT_TYPE): + """ + Send notify message, schedule its deletion. + + Args: + context (CallbackContext): The context object containing the job details and bot instance. + + Returns: + None + """ + bot, job = context.bot, context.job + async with session_scope() as sess: + chat_result = await sess.execute( + select(Chat).filter(Chat.id == job.data["chat_id"]) + ) + chat = chat_result.scalar_one_or_none() + + await _send_message_with_deletion( + context, + job.data.get("chat_id"), + job.data.get("user_id"), + chat.notify_message, + timeout_m=chat.kick_timeout - chat.notify_timeout, + ) + + +async def on_kick_timeout(context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Kick a user from the chat after a set amount of time and send a message about it. + + Args: + context (CallbackContext): The context object containing the job details and bot instance. + + Returns: + None + """ + bot, job = context.bot, context.job + + try: + await bot.ban_chat_member( + job.data.get("chat_id"), + job.data.get("user_id"), + until_date=datetime.now() + timedelta(seconds=60), + ) + ban_counter.add(1) + + async with session_scope() as sess: + chat_result = await sess.execute( + select(Chat).where(Chat.id == job.data["chat_id"]) + ) + chat = chat_result.scalar_one_or_none() + + if chat.on_kick_message.lower() not in ["false", "0"]: + await _send_message_with_deletion( + context, + job.data.get("chat_id"), + job.data.get("user_id"), + chat.on_kick_message, + ) + except Exception as e: + tg_logger.exception( + f"Failed to kick {job.data['user_id']} from {job.data['chat_id']}", + exc_info=e, + ) + await _send_message_with_deletion( + context, + job.data.get("chat_id"), + job.data.get("user_id"), + _("msg__failed_kick_response"), + ) + + +async def delete_message(context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Delete a message from a chat. + + Args: + context (CallbackContext): The context object containing the job details and bot instance. + + Returns: + None + """ + bot, job = context.bot, context.job + try: + await bot.delete_message(job.data["chat_id"], job.data["message_id"]) + except Exception as e: + tg_logger.warning( + f"can't delete {job.data['message_id']} from {job.data['chat_id']}", + exc_info=e, + ) + + +async def _mention_markdown(bot: Bot, chat_id: int, user_id: int, message: str) -> str: + """ + Format a message to include a markdown mention of a user. + + Args: + bot (Bot): The Telegram bot instance. + chat_id (int): The ID of the chat. + user_id (int): The ID of the user to mention. + message (str): The message to format. + + Returns: + str: The formatted message with the user mention. + """ + chat_member = await bot.get_chat_member(chat_id, user_id) + user = chat_member.user + # if not user.name: + # # если пользователь удален, у него пропадает имя и markdown выглядит так: (tg://user?id=666) + # user_mention_markdown = "" + # else: + user_mention_markdown = user.mention_markdown() + + # \ нужен из-за формата сообщений в маркдауне + tg_logger.warning(user_mention_markdown) + #user_mention_markdown = user_mention_markdown.replace("/[", "[") + #user_mention_markdown = user_mention_markdown.replace("]", "\]") + # wtf + message_mention = message.replace("%USER\\\\\\_MENTION%", user_mention_markdown) + message_mention = message_mention.replace("%USER\\\\_MENTION%", user_mention_markdown) + message_mention = message_mention.replace("%USER\\_MENTION%", user_mention_markdown) + message_mention = message_mention.replace("%USER_MENTION%", user_mention_markdown) + tg_logger.warning(message_mention) + return message_mention + + +async def _send_message_with_deletion( + context: ContextTypes.DEFAULT_TYPE, + chat_id: int, + user_id: int, + message: str, + timeout_m: int = constants.default_delete_message_timeout_m, + reply_to: Optional[Message] = None, +): + message_markdown = await _mention_markdown(context.bot, chat_id, user_id, message) + + if reply_to is not None: + sent_message = await reply_to.reply_text( + text=message_markdown, parse_mode=ParseMode.MARKDOWN + ) + else: + sent_message = await context.bot.send_message( + chat_id, text=message_markdown, parse_mode=ParseMode.MARKDOWN + ) + + # correctly handle negative timeouts + timeout_m = max(timeout_m, constants.default_delete_message_timeout_m) + + context.job_queue.run_once( + delete_message, + timeout_m * 60, + chat_id=chat_id, + user_id=user_id, + data={ + "chat_id": chat_id, + "user_id": user_id, + "message_id": sent_message.message_id, + }, + ) diff --git a/src/handlers/group/my_chat_member_handler.py b/src/handlers/group/my_chat_member_handler.py new file mode 100644 index 0000000..7a561f0 --- /dev/null +++ b/src/handlers/group/my_chat_member_handler.py @@ -0,0 +1,72 @@ +from telegram import ChatMember, Update +from telegram.ext import ContextTypes + +from sqlalchemy import select + +from src import constants +from src.model import Chat, User, session_scope +from src.handlers.admin.utils import new_keyboard_layout +from src.texts import _ + + +async def my_chat_member_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + old_status, new_status = update.my_chat_member.difference().get("status", (None, None)) + + if old_status == ChatMember.LEFT and new_status == ChatMember.MEMBER: + # which means the bot was added to the chat + await context.bot.send_message( + update.effective_chat.id, _("msg__add_bot_to_chat") + ) + return + + if ( + old_status != ChatMember.ADMINISTRATOR + and new_status == ChatMember.ADMINISTRATOR + ): + # which means the bot is now admin and can be used + async with session_scope() as sess: + result = await sess.execute( + select(Chat).filter_by(id=update.effective_chat.id) + ) + chat = result.scalars().first() + + if chat is None: + chat = Chat.get_new_chat(update.effective_chat.id) + sess.add(chat) + # hack with adding an empty #whois to prevent slow /start cmd + # TODO after v1.0: rework the DB schema + user = User( + chat_id=update.effective_chat.id, + user_id=update.effective_user.id, + whois="", + ) + await sess.merge(user) + # notify the admin about a new chat + button_configs = [ + [ + { + "text": "Приветствия", + "action": constants.Actions.set_intro_settings, + } + ], + [ + { + "text": "Удаление и блокировка", + "action": constants.Actions.set_kick_bans_settings, + } + ], + [{"text": "Назад", "action": constants.Actions.back_to_chats}], + ] + reply_markup = new_keyboard_layout( + button_configs, update.effective_chat.id + ) + await context.bot.send_message( + update.effective_user.id, + _("msg__make_admin_direct").format( + chat_name=update.effective_chat.title + ), + reply_markup=reply_markup, + ) + + await context.bot.send_message(update.effective_chat.id, _("msg__make_admin")) + return diff --git a/src/handlers/help_handler.py b/src/handlers/help_handler.py new file mode 100644 index 0000000..f694683 --- /dev/null +++ b/src/handlers/help_handler.py @@ -0,0 +1,9 @@ +from telegram import Update +from telegram.constants import ParseMode +from telegram.ext import ContextTypes + +from src.texts import _ + + +async def help_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await update.message.reply_text(_("msg__help"), parse_mode=ParseMode.MARKDOWN) diff --git a/src/handlers/utils.py b/src/handlers/utils.py new file mode 100644 index 0000000..a254bb3 --- /dev/null +++ b/src/handlers/utils.py @@ -0,0 +1,60 @@ +from functools import wraps + +from telegram import Update +from telegram.ext import CallbackContext +from opentelemetry import metrics + +from src.constants import DEBUG, TEAM_TELEGRAM_IDS +from src.model import Chat, User, session_scope + + +def setup_counter(meter_name, counter_name, version="2.0.0"): + """ + A helper function to remove duplication of code for counters creation. + """ + meter = metrics.get_meter(meter_name, version=version) + return meter.create_counter(counter_name, unit="1") + +def setup_histogram(meter_name, histogram_name, version="2.0.0"): + meter = metrics.get_meter(meter_name, version=version) + return meter.create_histogram(name=histogram_name) + + +def admin(func): + """ + A decorator to ensure that a particular function is only executed in private chats, + and not in group chats. + + Args: + func (Callable): The function to be wrapped by the decorator. + + Returns: + Callable: The wrapper function which includes the functionality for checking the chat type. + """ + + @wraps(func) + def wrapper(update: Update, context: CallbackContext, *args, **kwargs): + if update.message.chat_id < 0: + return # Skip the execution of the function in case of group chat + return func(update, context, *args, **kwargs) + + return wrapper + + +def debug(func): + """ + A decorator to ensure that a particular function is only executed for debug purposes, i.e. by someone from the team. + + Args: + func (Callable): The function to be wrapped by the decorator. + + Returns: + Callable: The wrapper function which includes the functionality for checking the called ID. + """ + + @wraps(func) + def wrapper(update: Update, context: CallbackContext, *args, **kwargs): + if DEBUG or update.message.chat_id in TEAM_TELEGRAM_IDS: + return func(update, context, *args, **kwargs) + + return wrapper diff --git a/src/logging.py b/src/logging.py new file mode 100644 index 0000000..823eaa8 --- /dev/null +++ b/src/logging.py @@ -0,0 +1,57 @@ +import logging +from logging import config +import os + +import grpc +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.resources import Resource + +dsn = os.environ.get("UPTRACE_DSN") + +resource = Resource( + attributes={"service.name": "wachter-bot", "service.version": "1.1.0", "deployment.environment": os.environ.get("DEPLOYMENT_ENVIRONMENT")} +) +logger_provider = LoggerProvider(resource=resource) +set_logger_provider(logger_provider) + +exporter = OTLPLogExporter( + endpoint="otlp.uptrace.dev:4317", + headers=(("uptrace-dsn", dsn),), + timeout=5, + compression=grpc.Compression.Gzip, +) +logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) + +log_config = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "wachter_telegram": { + "class": "telegram_handler.TelegramHandler", + "token": os.environ["TELEGRAM_TOKEN"], + "chat_id": os.environ["TELEGRAM_ERROR_CHAT_ID"], + }, + "wachter_oltp": { + "class": "opentelemetry.sdk._logs.LoggingHandler", + "level": logging.INFO, + "logger_provider": logger_provider, + }, + }, + "loggers": { + "wachter_telegram_logger": { + "level": "INFO", + "handlers": [ + "wachter_telegram", + "wachter_oltp", + ], + } + }, +} + +config.dictConfig(log_config) +tg_logger = logging.getLogger("wachter_telegram_logger") diff --git a/src/model.py b/src/model.py new file mode 100644 index 0000000..0fd386a --- /dev/null +++ b/src/model.py @@ -0,0 +1,107 @@ +from sqlalchemy import create_engine +from sqlalchemy import Column, Integer, Text, Boolean, BigInteger +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm.session import sessionmaker +from contextlib import asynccontextmanager + +from src import constants +from src.texts import _ + +Base = declarative_base() + + +class Chat(Base): + __tablename__ = "chats" + + id = Column(BigInteger, primary_key=True) + + on_new_chat_member_message = Column( + Text, + nullable=False, + ) + on_known_new_chat_member_message = Column( + Text, + nullable=False, + ) + on_introduce_message = Column( + Text, + nullable=False, + ) + on_kick_message = Column( + Text, + nullable=False, + ) + notify_message = Column( + Text, + nullable=False, + ) + regex_filter = Column(Text, nullable=True) # keeping that in db for now, unused + filter_only_new_users = Column( + Boolean, nullable=False, default=False + ) # keeping that in db for now, unused + kick_timeout = Column( + Integer, + nullable=False, + ) + notify_timeout = Column( + Integer, + nullable=False, + ) + whois_length = Column( + Integer, + nullable=False, + ) + on_introduce_message_update = Column( + Text, + nullable=False, + ) + + def __repr__(self): + return f"" + + @classmethod + def get_new_chat(cls, chat_id: int): + chat = cls(id=chat_id) + # write default values from texts + chat.on_new_chat_member_message = _("msg__new_chat_member") + chat.on_known_new_chat_member_message = _("msg__known_new_chat_member") + chat.on_introduce_message = _("msg__introduce") + chat.on_kick_message = _("msg__kick") + chat.notify_message = _("msg__notify") + chat.on_introduce_message_update = _("msg__introduce_update") + + chat.kick_timeout = constants.default_kick_timeout_m + chat.notify_timeout = constants.default_notify_timeout_m + chat.whois_length = constants.default_whois_length + return chat + + +class User(Base): + __tablename__ = "users" + + user_id = Column(BigInteger, primary_key=True) + chat_id = Column(BigInteger, primary_key=True) + + whois = Column(Text, nullable=False) + + +engine = create_async_engine(constants.get_uri(), echo=False) +AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +@asynccontextmanager +async def session_scope(): + async with AsyncSessionLocal() as session: + try: + yield session + await session.commit() + except: + await session.rollback() + raise + finally: + await session.close() + + +def orm_to_dict(obj): + return obj._asdict() diff --git a/src/texts.py b/src/texts.py new file mode 100644 index 0000000..0cbdbb8 --- /dev/null +++ b/src/texts.py @@ -0,0 +1,117 @@ +import re + +_texts = { + "msg__set_new_message": "Обновил сообщение.", + "msg__success_set_kick_timeout_response": "Обновил время до удаления.", + "msg__sucess_set_notify_timeout_response": "Обновил время до напоминания.", + "msg__failed_set_kick_timeout_response": "Время должно быть целым положительным числом.", + "msg__failed_kick_response": "Я не справился.", + "msg__start_command": "Выберите чат:", + "msg__select_chat": "Выбран чат {chat_name}. Теперь выберите действие:", + "msg__help": """Привет! Я - бот Вахтер. Я слежу, чтобы в твоем чате были только представившиеся пользователи. Для начала работы добавь меня в чат и сделай меня администратором. +После этого для настройки бота тебе нужно написать мне в личных сообщениях /start. +По умолчанию я не удаляю из чата непредставившихся, а лишь записываю все сообщения с хэштегом #whois. +Если ты хочешь автоматически удалять непредставившихся, то установи время ожидания до удаления из чата в значение больше нуля (в минутах). +По умолчанию за 10 минут до удаления я отправляю сообщение с напоминанием.""", + "msg__add_bot_to_chat": "Привет, я Вахтёр. Я буду следить за тем, чтобы все люди в чате были представившимися. Дайте мне админские права, чтобы я мог это делать.", + "msg__make_admin": "Спасибо, теперь я могу видеть сообщения. Пожалуйста, представьтесь, используя хэштег #whois.", + "msg__make_admin_direct": "Есть новый чат {chat_name}", + "msg__new_chat_member": "Добро пожаловать! Пожалуйста, представьтесь с использованием хэштега #whois и поздоровайтесь с сообществом.", + "msg__known_new_chat_member": "Добро пожаловать снова!", + "msg__introduce": "Спасибо и добро пожаловать!", + "msg__kick": "%USER\_MENTION% не представился и покидает чат.", + "msg__notify": "%USER\_MENTION%, пожалуйста, представьтесь с использованием хэштега #whois.", + "msg__introduce_update": "%USER\_MENTION%, если вы хотите обновить существующий #whois, пожалуйста добавьте тег #update к сообщению.", + "msg__no_chats_available": "У вас нет доступных чатов.", + "msg__sucess_whois_length": "Обновил необходимую длину #whois.", + "msg__failed_whois_response": "Длина должна быть целым положительным числом.", + "msg__need_hashtag_update_response": "Сообщение должно содержать #update.", + "btn__intro": "Приветствия", + "btn__kicks": "Удаление и блокировка", + "btn__back_to_chats": "Назад к списку чатов", + "btn__current_settings": "Посмотреть текущие настройки", + "btn__change_welcome_message": "Изменить сообщение при входе в чат", + "btn__change_rewelcome_message": "Изменить сообщение при перезаходе в чат", + "btn__change_notify_message": "Изменить сообщение напоминания", + "btn__change_sucess_message": "Изменить сообщение после представления", + "btn__change_notify_timeout": "Изменить время напоминания", + "btn__change_whois_length": "Изменить необходимую длину #whois", + "btn__change_whois_message": "Изменить сообщение для обновления #whois", + "btn__back": "Назад", + "btn__change_kick_timeout": "Изменить время до удаления", + "btn__change_kick_message": "Изменить сообщение после удаления", + "msg__set_new_welcome_message": "Отправьте новый текст сообщения при входе в чат. Используйте `%USER_MENTION%`, чтобы тегнуть адресата.", + "msg__set_new_kick_timout": "Отправьте новое время до удаления в минутах", + "msg__set_new_rewelcome_message": "Отправьте новый текст сообщения при перезаходе в чат. Используйте `%USER_MENTION%`, чтобы тегнуть адресата.", + "msg__set_new_notify_message": "Отправьте новый текст сообщения напоминания. Используйте `%USER_MENTION%`, чтобы тегнуть адресата.", + "msg__set_new_sucess_message": "Отправьте новый текст сообщения после представления. Используйте `%USER_MENTION%`, чтобы тегнуть адресата.", + "msg__set_new_whois_length": "Отправьте новую необходимую длину #whois (количество символов).", + "msg__set_new_kick_message": "Отправьте новый текст сообщения после удаления. Используйте `%USER_MENTION%`, чтобы тегнуть адресата.", + "msg__set_new_notify_timeout": "Отправьте новое время до напоминания в минутах.", + "msg__set_new_whois_message": "Отправьте новый текст сообщения для обновления #whois (должно содержать хэштег #update). Используйте `%USER_MENTION%`, чтобы тегнуть адресата.", + "msg__get_intro_settings": """ +Выбран чат {chat_name}. +--- +Сообщение для нового участника чата: `{on_new_chat_member_message}` +--- +Сообщение при перезаходе в чат: `{on_known_new_chat_member_message}` +--- +Сообщение после успешного представления: `{on_introduce_message}` +--- +Сообщение напоминания: `{notify_message}` +--- +Необходимая длина представления с хэштегом #whois для новых пользователей: {whois_length} +--- +Время до напоминания в минутах (целое положительное число): {notify_timeout} +--- +Сообщение для обновления информации в #whois: `{on_introduce_message_update}` +""", + "msg__get_kick_settings": """ +Выбран чат {chat_name}. +--- +Время до удаления в минутах (целое положительное число): {kick_timeout} +--- +Сообщение после удаления: `{on_kick_message}` +""", + "msg__short_whois": "%USER\_MENTION%, напишите про себя побольше, хотя бы {whois_length} символов. Спасибо!", + "msg__skip_new_chat_member": "%SKIP%", +} + +def escape_markdown(text): + """ + Escapes special characters in a Markdown string to prevent Markdown rendering issues, + excluding text within curly brackets. + + Args: + text (str): The input string that may contain special Markdown characters. + + Returns: + str: A string with special Markdown characters escaped, excluding text within curly brackets. + """ + # Regex to find text outside curly brackets + def escape_outside_braces(match): + text_outside = match.group(1) + if text_outside: + # Escape special characters in text outside curly brackets + special_characters = r"([\\`*_{}\[\]()#+\-.!|>~^])" + return re.sub(special_characters, r"\\\1", text_outside) + return match.group(0) + + # Match and process text outside curly brackets + escaped_text = re.sub(r"([^{}]+(?=\{)|(?<=\})([^{}]+)|^[^{]+|[^}]+$)", escape_outside_braces, text) + return escaped_text + +def _(text): + """ + Retrieve and escape a predefined message text based on a unique key. + + Args: + text (str): A unique key representing the desired message. + + Returns: + str: The escaped message text associated with the input key, or None if not found. + """ + return _texts.get(text) + # if raw_message is not None: + # return escape_markdown(raw_message) + # return None diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..c1827b9 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,180 @@ +import sys +import os +import pytest, pytest_asyncio, json +from unittest.mock import AsyncMock, MagicMock, patch +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import sessionmaker + +with pytest.MonkeyPatch().context() as ctx: + + def mock_get_uri(): + return "sqlite+aiosqlite:///:memory:?cache=shared" + + ctx.setattr("src.constants.get_uri", mock_get_uri) + from src import constants + from src.model import engine, User, Chat + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + + +@pytest.fixture +def mock_update(): + callback_query = AsyncMock() + callback_query.data = json.dumps({"action": constants.Actions.start_select_chat}) + + message_mock = MagicMock() + message_mock.chat_id = 12345 + message_mock.message_id = 67890 + message_mock.reply_text = AsyncMock() + + update = AsyncMock() + update.callback_query = callback_query + update.effective_message = message_mock + return update + + +@pytest.fixture +def mock_context(): + context = AsyncMock() + bot_mock = AsyncMock() + bot_mock.edit_message_text = AsyncMock() + context.job_queue.run_once = MagicMock() + context.bot = bot_mock + return context + + +# Fixture to set up an in-memory SQLite database +@pytest_asyncio.fixture(scope="function") +async def async_engine(): + async with engine.begin() as conn: + await conn.execute(text("DROP TABLE IF EXISTS users;")) + await conn.execute(text("DROP TABLE IF EXISTS chats;")) + await conn.execute( + text( + """ + CREATE TABLE chats ( + id INTEGER PRIMARY KEY, + on_new_chat_member_message TEXT NOT NULL, + on_known_new_chat_member_message TEXT NOT NULL, + on_introduce_message TEXT NOT NULL, + on_kick_message TEXT NOT NULL, + notify_message TEXT NOT NULL, + regex_filter TEXT, + filter_only_new_users BOOLEAN NOT NULL DEFAULT FALSE, + kick_timeout INTEGER NOT NULL, + notify_timeout INTEGER NOT NULL, + whois_length INTEGER NOT NULL, + on_introduce_message_update TEXT NOT NULL + ); + """ + ) + ) + await conn.execute( + text( + """ + CREATE TABLE users ( + user_id INTEGER, + chat_id INTEGER, + whois TEXT NOT NULL, + PRIMARY KEY (user_id, chat_id) + ); + """ + ) + ) + return engine + + +# Fixture for creating a new session for each test +@pytest_asyncio.fixture(scope="function") +async def async_session(async_engine): + async_session_local = sessionmaker( + async_engine, class_=AsyncSession, expire_on_commit=False + ) + async with async_session_local() as session: + yield session + + +@pytest_asyncio.fixture +async def populate_db(async_session): + # Define user data + user_data = [ + (1, 1, "User 1 in Chat 1"), + (1, 2, "User 1 in Chat 2"), + (2, 2, "User 2 in Chat 2"), + (2, 3, "User 2 in Chat 3"), + (3, None, "User 3, no Chats"), + ] + + # Define chat data + chat_data = [ + ( + 1, + "Welcome to Chat 1", + "Message for known members in Chat 1", + "Introduce in Chat 1", + "Kick message in Chat 1", + "Notify message in Chat 1", + None, + False, + 30, + 60, + 100, + "Update Introduce in Chat 1", + ), + ( + 2, + "Welcome to Chat 2", + "Message for known members in Chat 2", + "Introduce in Chat 2", + "Kick message in Chat 2", + "Notify message in Chat 2", + None, + False, + 30, + 60, + 100, + "Update Introduce in Chat 2", + ), + ( + -3, + "Welcome to Chat 2", + "Message for known members in Chat 2", + "Introduce in Chat 2", + "Kick message in Chat 2", + "Notify message in Chat 2", + None, + False, + 30, + 60, + 100, + "Update Introduce in Chat 2", + ), + ] + + # Create objects using list comprehension + users = [ + User(user_id=uid, chat_id=cid, whois=whois) for uid, cid, whois in user_data + ] + + chats = [ + Chat( + id=cid, + on_new_chat_member_message=welcome, + on_known_new_chat_member_message=known, + on_introduce_message=introduce, + on_kick_message=kick, + notify_message=notify, + regex_filter=regex, + filter_only_new_users=filter_new, + kick_timeout=kick_timeout, + notify_timeout=notify_timeout, + whois_length=whois_length, + on_introduce_message_update=introduce_update, + ) + for cid, welcome, known, introduce, kick, notify, regex, filter_new, kick_timeout, notify_timeout, whois_length, introduce_update in chat_data + ] + + async_session.add_all(users + chats) + + await async_session.commit() diff --git a/test/group_handler_test.py b/test/group_handler_test.py new file mode 100644 index 0000000..37349c8 --- /dev/null +++ b/test/group_handler_test.py @@ -0,0 +1,108 @@ +import pytest +from sqlalchemy import select +from unittest.mock import patch +import os + +with patch.dict( + "os.environ", + { + "TELEGRAM_TOKEN": "dummy_token", + "TELEGRAM_ERROR_CHAT_ID": "dummy_chat_id", + "UPTRACE_DSN": "dummy_dsn", + "DEPLOYMENT_ENVIRONMENT": "testing", + }, +): + from src.handlers.group.group_handler import on_hashtag_message + from src.model import User, Chat +from src.texts import _ + +from telegram.constants import ParseMode + + +async def mock_mention_markdown(bot, chat_id, user_id, message): + # Replace %USER_MENTION% with a default string + return message.replace("%USER_MENTION%", "@example_user") + + +@pytest.mark.asyncio +async def test_on_hashtag_message_new_user( + mock_update, mock_context, async_session, populate_db, mocker +): + # Simulate the Incoming Message + chat_id = -3 # Example group chat ID (negative for groups) + user_id = 3 # Example new user ID + mock_update.effective_message.chat_id = chat_id + mock_update.effective_message.from_user.id = user_id + mock_update.effective_message.text = "#whois I am new here dkgjldskfjglkdfjglkdfsj lgkjdsflökgjldösfjglsdfjgölkdsfjglöksdjfglöksdfjglöksdfjg" + + mocker.patch("src.handlers.group.group_handler.is_whois", return_value=True) + mocker.patch( + "src.handlers.group.group_handler._mention_markdown", + return_value="@example_user", + ) + mocker.patch( + "src.handlers.group.group_handler.remove_user_jobs_from_queue", + return_value=True, + ) + mocker.patch( + "src.handlers.group.group_handler.whois_counter.add", + return_value=True, + ) + + await on_hashtag_message(mock_update, mock_context) + + # Check if the reply message was sent + assert mock_update.effective_message.reply_text.await_count == 1 + + # Verify that the new user is added to the database + async with async_session as session: + result = await session.execute( + select(User).where(User.chat_id == chat_id, User.user_id == user_id) + ) + user = result.scalars().first() + assert user is not None + assert ( + user.whois + == "#whois I am new here dkgjldskfjglkdfjglkdfsj lgkjdsflökgjldösfjglsdfjgölkdsfjglöksdjfglöksdfjglöksdfjg" + ) + + +@pytest.mark.asyncio +async def test_on_hashtag_message_short_whois( + mock_update, mock_context, async_session, populate_db, mocker +): + # Simulate the Incoming Message + chat_id = -3 # Example group chat ID (negative for groups) + user_id = 3 # Example new user ID + mock_update.effective_message.chat_id = chat_id + mock_update.effective_message.from_user.id = user_id + mock_update.effective_message.text = "#whois I am new here" + + mocker.patch("src.handlers.group.group_handler.is_whois", return_value=True) + mocker.patch( + "src.handlers.group.group_handler._mention_markdown", + side_effect=mock_mention_markdown, + ) + + await on_hashtag_message(mock_update, mock_context) + + async with async_session as session: + try: + result = await session.execute( + select(Chat.whois_length).where(Chat.id == chat_id) + ) + whois_length = result.scalar_one() + print(whois_length) + except Exception as e: + print(f"Error during query execution: {e}") + raise + print() + expected_reply = _("msg__short_whois").format(whois_length=whois_length) + # Fetch the actual reply text + actual_reply_call = mock_update.effective_message.reply_text.call_args + actual_reply = actual_reply_call[1]["text"] if actual_reply_call else None + + # Improved assertion with detailed error message + assert ( + actual_reply == expected_reply + ), f"Assertion failed: Expected reply text '{expected_reply}' but got '{actual_reply}'." diff --git a/test/group_onboarding_test.py b/test/group_onboarding_test.py new file mode 100644 index 0000000..1cb8584 --- /dev/null +++ b/test/group_onboarding_test.py @@ -0,0 +1,81 @@ +import pytest +import json +from unittest.mock import AsyncMock, call + +from src.handlers.group.my_chat_member_handler import my_chat_member_handler +from src.texts import _ +from src import constants + +from telegram import ChatMember, InlineKeyboardButton, InlineKeyboardMarkup + + +@pytest.mark.asyncio +async def test_add_bot_to_chat(mock_context): + mock_update = AsyncMock() + mock_update.effective_chat.id = 1000 + mock_update.my_chat_member.difference = lambda: { + "status": (ChatMember.LEFT, ChatMember.MEMBER) + } + await my_chat_member_handler(mock_update, mock_context) + mock_context.bot.send_message.assert_awaited_once_with( + 1000, _("msg__add_bot_to_chat") + ) + + +@pytest.mark.asyncio +async def test_make_bot_admin(mock_context, async_session): + mock_update = AsyncMock() + mock_update.effective_chat.id = 1000 + mock_update.effective_chat.title = "title" + mock_update.effective_user.id = 1001 + mock_update.my_chat_member.difference = lambda: { + "status": (ChatMember.MEMBER, ChatMember.ADMINISTRATOR) + } + await my_chat_member_handler(mock_update, mock_context) + mock_context.bot.send_message.assert_has_calls( + [ + call( + 1001, + _("msg__make_admin_direct").format(chat_name="title"), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Приветствия", + callback_data=json.dumps( + { + "chat_id": 1000, + "action": constants.Actions.set_intro_settings, + } + ), + ) + ], + [ + InlineKeyboardButton( + "Удаление и блокировка", + callback_data=json.dumps( + { + "chat_id": 1000, + "action": constants.Actions.set_kick_bans_settings, + } + ), + ) + ], + [ + InlineKeyboardButton( + "Назад", + callback_data=json.dumps( + { + "chat_id": 1000, + "action": constants.Actions.back_to_chats, + } + ), + ) + ], + ] + ), + ), + call(1000, _("msg__make_admin")), + ], + any_order=True, + ) diff --git a/test/menu_handler_test.py b/test/menu_handler_test.py new file mode 100644 index 0000000..021a133 --- /dev/null +++ b/test/menu_handler_test.py @@ -0,0 +1,397 @@ +import json +import pytest, pytest_asyncio, asyncio + +# from conftest import function_scoped_event_loop, session_scoped_event_loop + +from src.handlers.admin.menu_handler import button_handler +from src.texts import _ +from src import constants + + +from unittest.mock import patch + +from telegram import InlineKeyboardButton, InlineKeyboardMarkup + +from telegram.constants import ParseMode + + +@pytest.mark.asyncio +async def test_button_handler_no_chats(mock_update, mock_context, populate_db): + mock_update.callback_query.from_user.id = 3 + await button_handler(mock_update, mock_context) + # Assert that the expected message was sent + mock_update.message.reply_text.assert_awaited_once_with( + _("msg__no_chats_available") + ) + + +@pytest.mark.asyncio +async def test_button_handler_basic_start(mock_update, mock_context, mocker): + mock_update.callback_query.from_user.id = 1 + mocker.patch("src.handlers.admin.utils.authorize_user", return_value=True) + mocker.patch("src.handlers.admin.utils.get_chat_name", return_value="Chat") + await button_handler(mock_update, mock_context) + mock_context.bot.edit_message_text.assert_awaited_once_with( + _("msg__start_command"), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Chat", + callback_data=json.dumps( + {"chat_id": 1, "action": constants.Actions.select_chat} + ), + ) + ], + [ + InlineKeyboardButton( + "Chat", + callback_data=json.dumps( + {"chat_id": 2, "action": constants.Actions.select_chat} + ), + ) + ], + ] + ), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + ) + + +@pytest.mark.asyncio +async def test_select_chat_action(mock_update, mock_context, mocker): + # Set the data for the select_chat action + action = constants.Actions.select_chat + selected_chat_id = 1 + chat_name = "Chat" + + # Mock the data being received from the callback_query + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + mocker.patch("src.handlers.admin.utils.get_chat_name", return_value="Chat") + + expected_keyboard = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=_("btn__intro"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.set_intro_settings, + } + ), + ) + ], + [ + InlineKeyboardButton( + text=_("btn__kicks"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.set_kick_bans_settings, + } + ), + ) + ], + [ + InlineKeyboardButton( + text=_("btn__back_to_chats"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.back_to_chats, + } + ), + ) + ], + ] + ) + + await button_handler(mock_update, mock_context) + actual_call = mock_context.bot.edit_message_text.call_args[ + 1 + ] # This gets the kwargs of the last call + assert actual_call["reply_markup"] == expected_keyboard + + +@pytest.mark.asyncio +async def test_set_kick_bans_settings(mock_update, mock_context, mocker): + action = constants.Actions.set_kick_bans_settings + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + await button_handler(mock_update, mock_context) + + expected_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text=_("btn__current_settings"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.get_current_kick_settings, + } + ), + ) + ], + [ + InlineKeyboardButton( + text=_("btn__change_kick_timeout"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.set_kick_timeout, + } + ), + ) + ], + [ + InlineKeyboardButton( + text=_("btn__change_kick_message"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.set_on_kick_message, + } + ), + ) + ], + [ + InlineKeyboardButton( + text=_("btn__back"), + callback_data=json.dumps( + { + "chat_id": selected_chat_id, + "action": constants.Actions.select_chat, + } + ), + ) + ], + ] + ) + + mock_context.bot.edit_message_reply_markup.assert_awaited_once_with( + reply_markup=expected_keyboard, + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + ) + + +@pytest.mark.asyncio +async def test_back_to_chats(mock_update, mock_context, mocker): + mock_update.callback_query.from_user.id = 1 + mocker.patch("src.handlers.admin.utils.authorize_user", return_value=True) + mocker.patch("src.handlers.admin.utils.get_chat_name", return_value="Chat") + await button_handler(mock_update, mock_context) + mock_context.bot.edit_message_text.assert_awaited_once_with( + _("msg__start_command"), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Chat", + callback_data=json.dumps( + {"chat_id": 1, "action": constants.Actions.select_chat} + ), + ) + ], + [ + InlineKeyboardButton( + "Chat", + callback_data=json.dumps( + {"chat_id": 2, "action": constants.Actions.select_chat} + ), + ) + ], + ] + ), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + ) + + +@pytest.mark.asyncio +async def test_set_on_new_chat_member_message_response( + mock_update, mock_context, mocker +): + action = constants.Actions.set_on_new_chat_member_message_response + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_welcome_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + + +@pytest.mark.asyncio +async def test_set_kick_timeout(mock_update, mock_context, mocker): + action = constants.Actions.set_kick_timeout + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_kick_timout"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + ) + + +@pytest.mark.asyncio +async def test_set_on_known_new_chat_member_message_response(mock_update, mock_context): + action = constants.Actions.set_on_known_new_chat_member_message_response + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_rewelcome_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + + +@pytest.mark.asyncio +async def test_set_new_notify_message(mock_update, mock_context, mocker): + action = constants.Actions.set_notify_message + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_notify_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + + +@pytest.mark.asyncio +async def test_set_on_new_chat_member_message_response( + mock_update, mock_context, mocker +): + action = constants.Actions.set_on_new_chat_member_message_response + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_welcome_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + + +@pytest.mark.asyncio +async def test_set_on_successful_introducion_response( + mock_update, mock_context, mocker +): + action = constants.Actions.set_on_successful_introducion_response + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_sucess_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + + +@pytest.mark.asyncio +async def test_set_whois_length(mock_update, mock_context, mocker): + action = constants.Actions.set_whois_length + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_whois_length"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + ) + + +@pytest.mark.asyncio +async def test_set_on_kick_message(mock_update, mock_context, mocker): + action = constants.Actions.set_on_kick_message + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_kick_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) + + +@pytest.mark.asyncio +async def test_set_notify_timeout(mock_update, mock_context, mocker): + action = constants.Actions.set_notify_timeout + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_notify_timeout"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + ) + + +@pytest.mark.asyncio +async def test_set_on_introduce_message_update(mock_update, mock_context, mocker): + action = constants.Actions.set_on_introduce_message_update + selected_chat_id = 1 + mock_update.callback_query.data = json.dumps( + {"action": action, "chat_id": selected_chat_id} + ) + + await button_handler(mock_update, mock_context) + + mock_context.bot.edit_message_text.assert_awaited_once_with( + text=_("msg__set_new_whois_message"), + chat_id=mock_update.callback_query.message.chat_id, + message_id=mock_update.callback_query.message.message_id, + parse_mode=ParseMode.MARKDOWN, + ) diff --git a/test/start_handler_test.py b/test/start_handler_test.py new file mode 100644 index 0000000..b412818 --- /dev/null +++ b/test/start_handler_test.py @@ -0,0 +1,43 @@ +import pytest +import json +from unittest.mock import patch + +from src.handlers.admin.start_handler import start_handler +from src import constants +from src.texts import _ + +from telegram import InlineKeyboardButton, InlineKeyboardMarkup + + +@pytest.mark.asyncio +async def test_start_handler_no_chats(mock_update, mock_context, populate_db): + mock_update.message.chat_id = 3 + await start_handler(mock_update, mock_context) + mock_update.message.reply_text.assert_awaited_once_with( + _("msg__no_chats_available") + ) + + +@pytest.mark.asyncio +async def test_start_handler_basic(mock_update, mock_context, mocker): + mock_update.message.chat_id = 1 + mocker.patch("src.handlers.admin.utils.authorize_user", return_value=True) + mocker.patch("src.handlers.admin.utils.get_chat_name", return_value="Chat") + await start_handler(mock_update, mock_context) + mock_update.message.reply_text.assert_awaited_once_with( + _("msg__start_command"), + reply_markup=InlineKeyboardMarkup( + inline_keyboard=( + ( + InlineKeyboardButton( + callback_data='{"chat_id": 1, "action": 2}', text="Chat" + ), + ), + ( + InlineKeyboardButton( + callback_data='{"chat_id": 2, "action": 2}', text="Chat" + ), + ), + ) + ), + ) diff --git a/wachter/actions.py b/wachter/actions.py deleted file mode 100644 index fec256f..0000000 --- a/wachter/actions.py +++ /dev/null @@ -1,773 +0,0 @@ -import json -import logging -from telegram import ( - Bot, - Update, - Message, - InlineKeyboardButton, - InlineKeyboardMarkup, - ParseMode, -) -from telegram.ext import Job, JobQueue -from telegram.error import TelegramError -from datetime import datetime, timedelta -from .model import Chat, User, session_scope, orm_to_dict -from . import constants -import re -import random -import typing - -logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO -) -logger = logging.getLogger(__name__) - - -def on_error(bot: Bot, update: Update, error: TelegramError): - logger.warning(f'Update "{update}" caused error "{error}"') - - -def authorize_user(bot: Bot, chat_id: int, user_id: int): - try: - status = bot.get_chat_member(chat_id, user_id).status - return status in ["creator", "administrator"] - except e: - return False - - -def mention_markdown(bot: Bot, chat_id: int, user_id: int, message: Message): - user = bot.get_chat_member(chat_id, user_id).user - if not user.name: - # если пользователь удален, у него пропадает имя и markdown выглядит так: (tg://user?id=666) - user_mention_markdown = "" - else: - user_mention_markdown = user.mention_markdown() - - # \ нужен из-за формата сообщений в маркдауне - return message.replace("%USER\_MENTION%", user_mention_markdown) - - -def on_help_command(bot: Bot, update: Update): - update.message.reply_text(constants.help_message) - - -def on_skip_command(bot: Bot, update: Update, job_queue: JobQueue): - chat_id = update.message.chat_id - - if chat_id > 0: - return - - if not update.message: - update.message = update.edited_message - - if update.message.reply_to_message is not None: - user_id = update.message.reply_to_message.from_user.id - - if not authorize_user(bot, chat_id, user_id): - return - removed = False - for job in job_queue.jobs(): - if ( - job.context["user_id"] == user_id - and job.context["chat_id"] == chat_id - and job.enabled == True - ): - try: - bot.delete_message( - job.context["chat_id"], job.context["message_id"] - ) - except: - pass - job.enabled = False - job.schedule_removal() - removed = True - if removed: - update.message.reply_text(constants.on_success_skip) - else: - update.message.reply_text(constants.on_failed_skip) - - -def on_new_chat_member(bot: Bot, update: Update, job_queue: JobQueue): - chat_id = update.message.chat_id - user_id = update.message.new_chat_members[-1].id - - for job in job_queue.jobs(): - if ( - job.context["user_id"] == user_id - and job.context["chat_id"] == chat_id - and job.enabled == True - ): - job.enabled = False - job.schedule_removal() - - with session_scope() as sess: - user = ( - sess.query(User) - .filter(User.chat_id == chat_id, User.user_id == user_id) - .first() - ) - chat = sess.query(Chat).filter(Chat.id == chat_id).first() - - if chat is None: - chat = Chat(id=chat_id) - sess.add(chat) - sess.commit() - - if user is not None: - update.message.reply_text(chat.on_known_new_chat_member_message) - return - - message = chat.on_new_chat_member_message - timeout = chat.kick_timeout - - if message == constants.skip_on_new_chat_member_message: - return - - message_markdown = mention_markdown(bot, chat_id, user_id, message) - msg = update.message.reply_text(message_markdown, parse_mode=ParseMode.MARKDOWN) - - if timeout != 0: - if timeout >= 10: - job = job_queue.run_once( - on_notify_timeout, - (timeout - constants.notify_delta) * 60, - context={ - "chat_id": chat_id, - "user_id": user_id, - "job_queue": job_queue, - }, - ) - - job = job_queue.run_once( - on_kick_timeout, - timeout * 60, - context={ - "chat_id": chat_id, - "user_id": user_id, - "message_id": msg.message_id, - }, - ) - - -def on_notify_timeout(bot: Bot, job: Job): - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == job.context["chat_id"]).first() - - message_markdown = mention_markdown( - bot, job.context["chat_id"], job.context["user_id"], chat.notify_message - ) - - message = bot.send_message( - job.context["chat_id"], text=message_markdown, parse_mode=ParseMode.MARKDOWN - ) - - job.context["job_queue"].run_once( - delete_message, - constants.notify_delta * 60, - context={ - "chat_id": job.context["chat_id"], - "user_id": job.context["user_id"], - "message_id": message.message_id, - }, - ) - - -def delete_message(bot: Bot, job: Job): - logger.info("delete called") - try: - bot.delete_message(job.context["chat_id"], job.context["message_id"]) - logger.info("delete sucess") - except: - logger.info("suck") - print(f"can't delete {job.context['message_id']} from {job.context['chat_id']}") - - -def on_kick_timeout(bot: Bot, job: Job): - try: - bot.delete_message(job.context["chat_id"], job.context["message_id"]) - except: - pass - - try: - bot.kick_chat_member( - job.context["chat_id"], - job.context["user_id"], - until_date=datetime.now() + timedelta(seconds=60), - ) - - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == job.context["chat_id"]).first() - - if chat.on_kick_message.lower() not in ["false", "0"]: - message_markdown = mention_markdown( - bot, - job.context["chat_id"], - job.context["user_id"], - chat.on_kick_message, - ) - if job.context["chat_id"] == constants.RH_CHAT_ID: - message_markdown = mention_markdown( - bot, - job.context["chat_id"], - job.context["user_id"], - random.choice(constants.RH_kick_messages), - ) - bot.send_message( - job.context["chat_id"], - text=message_markdown, - parse_mode=ParseMode.MARKDOWN, - ) - except Exception as e: - logging.error(e) - bot.send_message(job.context["chat_id"], text=constants.on_failed_kick_response) - - -def on_hashtag_message(bot: Bot, update: Update, user_data: dict, job_queue: JobQueue): - if not update.message: - update.message = update.edited_message - - chat_id = update.message.chat_id - - if ( - "#whois" in update.message.parse_entities(types=["hashtag"]).values() - and len(update.message.text) >= constants.min_whois_length - and chat_id < 0 - ): - user_id = update.message.from_user.id - - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == chat_id).first() - - if chat is None: - chat = Chat(id=chat_id) - sess.add(chat) - sess.commit() - - message = chat.on_introduce_message - - with session_scope() as sess: - user = User(chat_id=chat_id, user_id=user_id, whois=update.message.text) - sess.merge(user) - - removed = False - for job in job_queue.jobs(): - if ( - job.context["user_id"] == user_id - and job.context["chat_id"] == chat_id - and job.enabled == True - ): - try: - bot.delete_message( - job.context["chat_id"], job.context["message_id"] - ) - except: - pass - job.enabled = False - job.schedule_removal() - removed = True - - if removed: - message_markdown = mention_markdown(bot, chat_id, user_id, message) - update.message.reply_text(message_markdown, parse_mode=ParseMode.MARKDOWN) - - else: - on_message(bot, update, user_data=user_data, job_queue=job_queue) - - -def get_chats(users: list, user_id: int, bot: Bot): - for x in users: - try: - if authorize_user(bot, x.chat_id, user_id): - yield { - "title": bot.get_chat(x.chat_id).title or x.chat_id, - "id": x.chat_id, - } - except Exception: - pass - - -def on_start_command(bot: Bot, update: Update, user_data: dict): - user_id = update.message.chat_id - - if user_id < 0: - return - - with session_scope() as sess: - users = sess.query(User).filter(User.user_id == user_id) - user_chats = list(get_chats(users, user_id, bot)) - - if len(user_chats) == 0: - update.message.reply_text("У вас нет доступных чатов.") - return - - keyboard = [ - [ - InlineKeyboardButton( - chat["title"], - callback_data=json.dumps( - {"chat_id": chat["id"], "action": constants.Actions.select_chat} - ), - ) - ] - for chat in user_chats - if authorize_user(bot, chat["id"], user_id) - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text(constants.on_start_command, reply_markup=reply_markup) - - -def on_button_click(bot: Bot, update: Update, user_data: dict): - query = update.callback_query - data = json.loads(query.data) - - if data["action"] == constants.Actions.start_select_chat: - with session_scope() as sess: - user_id = query.from_user.id - users = sess.query(User).filter(User.user_id == user_id) - user_chats = [ - {"title": bot.get_chat(x.chat_id).title or x.chat_id, "id": x.chat_id} - for x in users - ] - - if len(user_chats) == 0: - update.message.reply_text("У вас нет доступных чатов.") - return - - keyboard = [ - [ - InlineKeyboardButton( - chat["title"], - callback_data=json.dumps( - {"chat_id": chat["id"], "action": constants.Actions.select_chat} - ), - ) - ] - for chat in user_chats - if authorize_user(bot, chat["id"], user_id) - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - bot.edit_message_reply_markup( - reply_markup=reply_markup, - chat_id=query.message.chat_id, - message_id=query.message.message_id, - ) - - if data["action"] == constants.Actions.select_chat: - selected_chat_id = data["chat_id"] - keyboard = [ - [ - InlineKeyboardButton( - "Изменить таймаут кика", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_kick_timeout, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить сообщение при входе в чат", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_on_new_chat_member_message_response, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить сообщение при перезаходе в чат", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_on_known_new_chat_member_message_response, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить сообщение после успешного представления", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_on_successful_introducion_response, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить сообщение напоминания", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_notify_message, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить сообщение после кика", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_on_kick_message, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить regex для фильтра сообщений", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_regex_filter, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Изменить фильтрацию только для новых пользователей", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_filter_only_new_users, - } - ), - ) - ], - [ - InlineKeyboardButton( - "Получить текущие настройки", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.get_current_settings, - } - ), - ) - ], - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - bot.edit_message_reply_markup( - reply_markup=reply_markup, - chat_id=query.message.chat_id, - message_id=query.message.message_id, - ) - - elif data["action"] in [ - constants.Actions.set_on_new_chat_member_message_response, - constants.Actions.set_kick_timeout, - constants.Actions.set_notify_message, - constants.Actions.set_on_known_new_chat_member_message_response, - constants.Actions.set_on_successful_introducion_response, - constants.Actions.set_on_kick_message, - constants.Actions.set_regex_filter, - constants.Actions.set_filter_only_new_users, - ]: - bot.edit_message_text( - text="Отправьте новое значение", - chat_id=query.message.chat_id, - message_id=query.message.message_id, - ) - user_data["chat_id"] = data["chat_id"] - user_data["action"] = data["action"] - - elif data["action"] == constants.Actions.get_current_settings: - keyboard = [ - [ - InlineKeyboardButton( - "К настройке чата", - callback_data=json.dumps( - { - "chat_id": data["chat_id"], - "action": constants.Actions.select_chat, - } - ), - ), - InlineKeyboardButton( - "К списку чатов", - callback_data=json.dumps( - {"action": constants.Actions.start_select_chat} - ), - ), - ], - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == data["chat_id"]).first() - bot.edit_message_text( - text=constants.get_settings_message.format(**chat.__dict__), - parse_mode=ParseMode.MARKDOWN, - chat_id=query.message.chat_id, - message_id=query.message.message_id, - reply_markup=reply_markup, - ) - - user_data["action"] = None - - -def filter_message(chat_id: int, message: Message): - if not message: - return False - - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == chat_id).first() - - if chat.regex_filter is None: - return False - else: - return re.search(chat.regex_filter, message) - - -def on_forward(bot: Bot, update: Update, job_queue: JobQueue): - chat_id = update.message.chat_id - user_id = update.message.from_user.id - removed = False - - if chat_id < 0 and not authorize_user(bot, chat_id, user_id): - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == chat_id == chat_id).first() - if chat.regex_filter is None: - return - - for job in job_queue.jobs(): - if ( - job.context["user_id"] == user_id - and job.context["chat_id"] == chat_id - and job.enabled == True - ): - removed = True - try: - bot.delete_message( - job.context["chat_id"], job.context["message_id"] - ) - except: - pass - job.enabled = False - job.schedule_removal() - - if removed: - bot.delete_message(chat_id, update.message.message_id) - message_markdown = mention_markdown( - bot, chat_id, user_id, constants.on_filtered_message - ) - message = bot.send_message( - chat_id, text=message_markdown, parse_mode=ParseMode.MARKDOWN - ) - bot.kick_chat_member( - chat_id, user_id, until_date=datetime.now() + timedelta(seconds=60) - ) - - -def is_new_user(chat_id: int, user_id: int): - with session_scope() as sess: - # if user is not in database he hasn't introduced himself with #whois - user = ( - sess.query(User) - .filter(User.user_id == user_id, User.chat_id == chat_id) - .first() - ) - is_new = not user - return is_new - - -def is_chat_filters_new_users(chat_id: int): - with session_scope() as sess: - filter_only_new_users = ( - sess.query(Chat.filter_only_new_users).filter(Chat.id == chat_id).first() - ) - return filter_only_new_users - - -def on_message(bot: Bot, update: Update, user_data: dict, job_queue: JobQueue): - if not update.message: - update.message = update.edited_message - - chat_id = update.message.chat_id - - if chat_id < 0: - user_id = update.message.from_user.id - if update.message.forward_from: - on_forward(bot, update, job_queue) - return - - message_text = update.message.text or update.message.caption - filter_mask = not authorize_user(bot, chat_id, user_id) and filter_message( - chat_id, message_text - ) - - if is_chat_filters_new_users(chat_id): - filter_mask = filter_mask and is_new_user(chat_id, user_id) - - if filter_mask: - bot.delete_message(chat_id, update.message.message_id) - message_markdown = mention_markdown( - bot, chat_id, user_id, constants.on_filtered_message - ) - for job in job_queue.jobs(): - if ( - job.context["user_id"] == user_id - and job.context["chat_id"] == chat_id - and job.enabled == True - ): - try: - bot.delete_message( - job.context["chat_id"], job.context["message_id"] - ) - except: - pass - job.enabled = False - job.schedule_removal() - message = bot.send_message( - chat_id, text=message_markdown, parse_mode=ParseMode.MARKDOWN - ) - bot.kick_chat_member( - chat_id, user_id, until_date=datetime.now() + timedelta(seconds=60) - ) - else: - user_id = chat_id - action = user_data.get("action") - - if action is None: - return - - chat_id = user_data["chat_id"] - - if action == constants.Actions.set_kick_timeout: - message = update.message.text - try: - timeout = int(message) - assert timeout >= 0 - except: - update.message.reply_text(constants.on_failed_set_kick_timeout_response) - return - - with session_scope() as sess: - chat = Chat(id=chat_id, kick_timeout=timeout) - sess.merge(chat) - user_data["action"] = None - - keyboard = [ - [ - InlineKeyboardButton( - "К настройке чата", - callback_data=json.dumps( - { - "chat_id": chat_id, - "action": constants.Actions.select_chat, - } - ), - ), - InlineKeyboardButton( - "К списку чатов", - callback_data=json.dumps( - {"action": constants.Actions.start_select_chat} - ), - ), - ], - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - constants.on_success_set_kick_timeout_response, - reply_markup=reply_markup, - ) - - elif action in [ - constants.Actions.set_on_new_chat_member_message_response, - constants.Actions.set_notify_message, - constants.Actions.set_on_known_new_chat_member_message_response, - constants.Actions.set_on_successful_introducion_response, - constants.Actions.set_on_kick_message, - constants.Actions.set_regex_filter, - constants.Actions.set_filter_only_new_users, - ]: - message = update.message.text_markdown - with session_scope() as sess: - if action == constants.Actions.set_on_new_chat_member_message_response: - chat = Chat(id=chat_id, on_new_chat_member_message=message) - if ( - action - == constants.Actions.set_on_known_new_chat_member_message_response - ): - chat = Chat(id=chat_id, on_known_new_chat_member_message=message) - if action == constants.Actions.set_on_successful_introducion_response: - chat = Chat(id=chat_id, on_introduce_message=message) - if action == constants.Actions.set_notify_message: - chat = Chat(id=chat_id, notify_message=message) - if action == constants.Actions.set_on_kick_message: - chat = Chat(id=chat_id, on_kick_message=message) - if action == constants.Actions.set_filter_only_new_users: - if message.lower() in ["true", "1"]: - filter_only_new_users = True - else: - filter_only_new_users = False - chat = Chat(id=chat_id, filter_only_new_users=filter_only_new_users) - - if action == constants.Actions.set_regex_filter: - if message == "%TURN_OFF%": - chat = Chat(id=chat_id, regex_filter=None) - else: - message = update.message.text - chat = Chat(id=chat_id, regex_filter=message) - sess.merge(chat) - - user_data["action"] = None - - keyboard = [ - [ - InlineKeyboardButton( - "К настройке чата", - callback_data=json.dumps( - { - "chat_id": chat_id, - "action": constants.Actions.select_chat, - } - ), - ), - InlineKeyboardButton( - "К списку чатов", - callback_data=json.dumps( - {"action": constants.Actions.start_select_chat} - ), - ), - ], - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - constants.on_set_new_message, reply_markup=reply_markup - ) - - -def on_whois_command(bot: Bot, update: Update, args: list): - if len(args) != 1: - update.message.reply_text("Usage: /whois ") - - chat_id = update.message.chat_id - user_id = args[0] # TODO: Use username instead of user_id - - with session_scope() as sess: - user = ( - sess.query(User) - .filter(User.chat_id == chat_id, User.user_id == user_id) - .first() - ) - - if user is None: - update.message.reply_text("user not found") - return - - update.message.reply_text(f"whois: {user.whois}") diff --git a/wachter/bot.py b/wachter/bot.py deleted file mode 100644 index 0b6cd0c..0000000 --- a/wachter/bot.py +++ /dev/null @@ -1,64 +0,0 @@ -from telegram.ext import ( - Updater, - CommandHandler, - Filters, - MessageHandler, - CallbackQueryHandler, -) -from .custom_filters import filter_bot_added -from . import actions -import os - - -def main(): - updater = Updater(os.environ["TELEGRAM_TOKEN"]) - dp = updater.dispatcher - - dp.add_handler(CommandHandler("help", actions.on_help_command)) - dp.add_error_handler(actions.on_error) - - dp.add_handler( - MessageHandler( - Filters.status_update.new_chat_members & filter_bot_added, - actions.on_new_chat_member, - pass_job_queue=True, - ) - ) - dp.add_handler( - MessageHandler( - Filters.entity("hashtag"), - actions.on_hashtag_message, - pass_job_queue=True, - edited_updates=True, - pass_user_data=True, - ) - ) - dp.add_handler( - MessageHandler(Filters.forwarded, actions.on_forward, pass_job_queue=True) - ) - - dp.add_handler( - CommandHandler("start", actions.on_start_command, pass_user_data=True) - ) - dp.add_handler( - CommandHandler( - "skip", actions.on_skip_command, allow_edited=True, pass_job_queue=True - ) - ) - dp.add_handler(CallbackQueryHandler(actions.on_button_click, pass_user_data=True)) - dp.add_handler( - MessageHandler( - (Filters.text | Filters.entity), - actions.on_message, - allow_edited=True, - pass_user_data=True, - pass_job_queue=True, - ) - ) - - updater.start_polling() - updater.idle() - - -if __name__ == "__main__": - main() diff --git a/wachter/constants.py b/wachter/constants.py deleted file mode 100644 index 12f7605..0000000 --- a/wachter/constants.py +++ /dev/null @@ -1,69 +0,0 @@ -from enum import IntEnum, auto - -# MESSAGES -on_set_new_message = "Обновил сообщение." -on_success_set_kick_timeout_response = "Обновил таймаут кика." -on_failed_set_kick_timeout_response = "Таймаут должен быть целым положительным числом" -on_failed_kick_response = "Я не справился." -on_failed_skip = "Ответьте на сообщение пользователя которого не нужно кикать" -on_success_skip = "Теперь пользователю не нужно представляться." -on_success_kick_response = "%USER\_MENTION% не представился и был кикнут из чата." -on_start_command = "Выберите чат и действие:" -on_filtered_message = "%USER\_MENTION%, вы были забанены т.к ваше сообщение содержит репост или слово из спам листа" -skip_on_new_chat_member_message = "%SKIP%" -help_message = """Привет. Для начала работы добавь меня в чат. -Для настройки бота админу нужно представиться в чате (написать сообщение с #whois длинной больше 120 символов) и написать мне в личных сообщениях /start. -По умолчанию я не кикаю непредставившихся, а лишь записываю все сообщения с тегом #whois. -Если нужно кикать, то установи таймаут кика в значение больше нуля (в минутах). -За 10 минут до кика я отправляю сообщение с напоминанием. -""" - -get_settings_message = """ -Таймаут кика: {kick_timeout} ---- -Сообщение для нового участника чата: {on_new_chat_member_message} ---- -Сообщение при перезаходе в чат: {on_known_new_chat_member_message} ---- -Сообщение после успешного представления: {on_introduce_message} ---- -Сообщение предупреждения: {notify_message} ---- -Regex фильтр: ```{regex_filter}``` ---- -Сообщение после кика: {on_kick_message} -––– -Кикать по regex только новых: {filter_only_new_users} -""" - -default_kick_timeout = 0 -notify_delta = 10 -min_whois_length = 60 - - -# ACTIONS -class Actions(IntEnum): - start_select_chat = auto() - select_chat = auto() - set_on_new_chat_member_message_response = auto() - set_notify_message = auto() - set_on_successful_introducion_response = auto() - set_on_known_new_chat_member_message_response = auto() - set_kick_timeout = auto() - set_on_kick_message = auto() - set_regex_filter = auto() - set_filter_only_new_users = auto() - get_current_settings = auto() - - -RH_kick_messages = [ - "Хакер %USER\_MENTION% молчит и покидает чат. ⚰", - "Хакера %USER\_MENTION% забрал роскомнадзор", - "Хакера %USER\_MENTION% забрал Интерпол", - "Хакер %USER\_MENTION% провалил дедлайн", - "Хакер %USER\_MENTION% не смог выйти из VIM", - "Хакер %USER\_MENTION% пошёл кормить рыбок", - "Хакер %USER\_MENTION% провалил испытание", -] - -RH_CHAT_ID = -1001147286684 diff --git a/wachter/model.py b/wachter/model.py deleted file mode 100644 index 04cc51c..0000000 --- a/wachter/model.py +++ /dev/null @@ -1,74 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy import Column, Integer, String, Text, Boolean, BigInteger -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm.session import sessionmaker -from contextlib import contextmanager -import enum -import os -from sqlalchemy import inspect - -Base = declarative_base() - - -class Chat(Base): - __tablename__ = "chats" - - id = Column(BigInteger, primary_key=True) - - on_new_chat_member_message = Column( - Text, - nullable=False, - default="Пожалуйста, представьтесь и поздоровайтесь с сообществом.", - ) - on_known_new_chat_member_message = Column( - Text, nullable=False, default="Добро пожаловать. Снова" - ) - on_introduce_message = Column(Text, nullable=False, default="Добро пожаловать.") - on_kick_message = Column( - Text, nullable=False, default="%USER\_MENTION% молчит и покидает чат" - ) - notify_message = Column( - Text, - nullable=False, - default="%USER\_MENTION%, пожалуйста, представьтесь и поздоровайтесь с сообществом.", - ) - regex_filter = Column(Text, nullable=True) - filter_only_new_users = Column(Boolean, nullable=False, default=False) - kick_timeout = Column(Integer, nullable=False, default=0) - - def __repr__(self): - return f"" - - -class User(Base): - __tablename__ = "users" - - user_id = Column(BigInteger, primary_key=True) - chat_id = Column(BigInteger, primary_key=True) - - whois = Column(Text, nullable=False) - - -def get_uri(): - return os.environ.get("DATABASE_URL", "postgresql://localhost:5432/wachter") - - -engine = create_engine(get_uri(), echo=False) -Session = sessionmaker(autoflush=True, bind=engine) - - -@contextmanager -def session_scope(): - session = Session() - try: - yield session - session.commit() - except: - session.rollback() - raise - finally: - session.close() - - -def orm_to_dict(obj): - return obj._asdict()