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 new file mode 100644 index 0000000..1d926c4 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,51 @@ +name: CI + +on: + pull_request: + branches: + - dev + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - 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 1867b97..45304e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,12 @@ 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 app +RUN ["chmod", "+x", "/app/entrypoint.sh"] +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/Pipfile b/Pipfile index f92dfd0..1742b3b 100644 --- a/Pipfile +++ b/Pipfile @@ -4,17 +4,29 @@ verify_ssl = true name = "pypi" [packages] -"psycopg2-binary" = "*" -python-telegram-bot = {version = "~=13.11"} sqlalchemy = "*" alembic = "*" pre-commit = "*" pylint = "*" black = "*" -pyTelegramLogger = "*" +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.9" \ No newline at end of file +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 61a4d19..b9eab33 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "aa2f3b9d31e5fb1bbb24a940ae90ae26d170f5c2ee21fa86e0df73b0def46b0d" + "sha256": "e903d17cd9d86285f15e7184fec65b13946f3869872b68f900a5f8df98e9a1f9" }, "pipfile-spec": 6, "requires": { @@ -16,74 +16,148 @@ ] }, "default": { + "aiosqlite": { + "hashes": [ + "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", + "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, "alembic": { "hashes": [ - "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f", - "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b" + "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2", + "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.12.0" + "markers": "python_version >= '3.8'", + "version": "==1.13.3" + }, + "anyio": { + "hashes": [ + "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", + "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" + ], + "version": "==4.6.2.post1" }, "apscheduler": { "hashes": [ - "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244", - "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526" + "sha256:e6df071b27d9be898e486bc7940a7be50b4af2e9da7c08f0744a96d4bd4cef4a", + "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661" ], - "version": "==3.6.3" + "version": "==3.10.4" }, "astroid": { "hashes": [ - "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c", - "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd" + "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d", + "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8" ], - "markers": "python_full_version >= '3.7.2'", - "version": "==2.15.6" + "markers": "python_full_version >= '3.9.0'", + "version": "==3.3.5" }, - "black": { + "async-timeout": { "hashes": [ - "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", - "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7", - "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100", - "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573", - "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d", - "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f", - "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9", - "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300", - "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948", - "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325", - "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9", - "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71", - "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186", - "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f", - "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe", - "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855", - "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80", - "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393", - "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c", - "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204", - "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377", - "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" + "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", - "markers": "python_version >= '3.8'", - "version": "==23.9.1" + "version": "==3.4.3" }, - "cachetools": { + "asyncpg": { "hashes": [ - "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", - "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" + "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" ], - "markers": "python_version ~= '3.5'", - "version": "==4.2.2" + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==0.29.0" + }, + "black": { + "hashes": [ + "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", + "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": [ @@ -95,84 +169,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", - "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", - "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", - "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", - "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", - "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", - "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", - "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", - "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", - "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", - "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", - "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", - "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", - "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", - "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", - "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", - "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", - "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", - "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", - "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", - "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", - "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", - "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", - "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", - "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", - "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", - "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", - "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", - "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", - "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", - "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", - "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", - "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", - "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", - "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", - "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", - "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", - "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", - "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", - "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", - "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", - "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", - "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", - "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", - "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", - "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", - "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", - "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", - "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", - "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", - "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", - "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", - "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", - "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", - "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", - "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", - "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", - "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", - "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", - "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", - "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", - "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", - "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", - "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", - "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", - "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", - "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", - "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", - "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", - "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", - "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", - "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", - "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", - "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", - "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + "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.2.0" + "version": "==3.4.0" }, "click": { "hashes": [ @@ -182,238 +286,335 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "deprecated": { + "hashes": [ + "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", + "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" + ], + "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:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", - "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", + "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c" ], "markers": "python_version < '3.11'", - "version": "==0.3.7" + "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:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", - "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], "markers": "python_version >= '3.8'", - "version": "==3.12.4" + "version": "==3.16.1" + }, + "googleapis-common-protos": { + "hashes": [ + "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", + "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" + ], + "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": "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:24437fbf6f4d3fe6efd0eb9d67e24dd9106db99af5ceb27996a5f7895f24bf1b", - "sha256:d43d52b86b15918c137e3a74fff5224f60385cd0e9c38e99d07c257f02f151a5" + "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", + "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" ], "markers": "python_version >= '3.8'", - "version": "==2.5.29" + "version": "==2.6.1" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.5'", - "version": "==3.4" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, - "isort": { + "importlib-metadata": { "hashes": [ - "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", - "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", + "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], - "markers": "python_full_version >= '3.8.0'", - "version": "==5.12.0" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", - "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", - "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", - "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", - "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", - "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", - "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", - "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", - "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", - "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", - "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", - "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", - "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", - "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", - "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", - "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", - "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", - "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", - "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", - "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", - "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", - "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", - "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", - "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", - "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", - "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", - "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", - "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", - "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", - "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", - "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", - "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", - "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", - "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", - "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", - "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" + "markers": "python_version >= '3.8'", + "version": "==8.4.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], "markers": "python_version >= '3.7'", - "version": "==1.9.0" + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" }, "mako": { "hashes": [ - "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", - "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" + "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a", + "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.5" }, "markupsafe": { "hashes": [ - "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", - "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", - "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", - "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", - "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", - "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", - "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", - "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", - "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", - "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", - "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", - "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", - "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", - "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", - "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", - "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", - "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", - "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", - "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", - "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", - "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", - "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", - "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", - "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", - "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", - "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + "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.7'", - "version": "==2.1.3" + "markers": "python_version >= '3.9'", + "version": "==3.0.1" }, "mccabe": { "hashes": [ @@ -433,215 +634,345 @@ }, "nodeenv": { "hashes": [ - "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", - "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" ], "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.8.0" + "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:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==23.1" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ - "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", - "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "markers": "python_version >= '3.7'", - "version": "==0.11.2" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "platformdirs": { "hashes": [ - "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", - "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], - "markers": "python_version >= '3.7'", - "version": "==3.10.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:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522", - "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945" + "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", + "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878" ], "index": "pypi", + "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": "==3.4.0" + "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", - "markers": "python_version >= '3.6'", - "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:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413", - "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252" + "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9", + "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e" ], "index": "pypi", - "markers": "python_full_version >= '3.7.2'", - "version": "==2.17.5" + "markers": "python_full_version >= '3.9.0'", + "version": "==3.3.1" }, - "pytelegramlogger": { + "pytest": { "hashes": [ - "sha256:313b3aa2aed283a65d255e2353b0bf34bfc2df547240844def30cb65a3f203e3", - "sha256:f0845c7651a367bce50a407059006a1dfbd2d84a7f9f0cb713b527053de73eb0" + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" ], - "markers": "python_full_version >= '3.6.0'", - "version": "==1.0.4" + "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", + "markers": "python_version >= '3.8'", + "version": "==3.14.0" }, "python-telegram-bot": { + "extras": [ + "job-queue" + ], "hashes": [ - "sha256:06780c258d3f2a3c6c79a7aeb45714f4cd1dd6275941b7dc4628bba64fddd465", - "sha256:b4047606b8081b62bbd6aa361f7ca1efe87fa8f1881ec9d932d35844bf57a154" + "sha256:8b2b37836c3ff9c2924e990474a1c4731df21b1668acebff5099f475666426c6", + "sha256:f2d6431bf154a53f40cdfc6c1d492a66102c0e4938709f6d8202bcd951c840cb" ], - "markers": "python_version >= '3.7'", - "version": "==13.15" + "markers": "python_version >= '3.8'", + "version": "==21.6" }, - "pytz": { + "python-telegram-handler": { "hashes": [ - "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", - "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + "sha256:f6e9ca60e15fa4e4595e323cc57362fe20cca3ca16e06158ad726caa48b3b16e" ], - "version": "==2023.3.post1" + "index": "pypi", + "version": "==2.2.1" }, - "pyyaml": { + "pytz": { "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:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "version": "==2024.2" }, - "requests": { + "pyyaml": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "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.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, - "setuptools": { + "requests": { "hashes": [ - "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", - "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "markers": "python_version >= '3.8'", - "version": "==68.2.2" + "version": "==2.32.3" }, "six": { "hashes": [ @@ -651,256 +982,236 @@ "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:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e", - "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6", - "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258", - "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce", - "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede", - "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce", - "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4", - "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4", - "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5", - "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01", - "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9", - "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9", - "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67", - "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9", - "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9", - "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301", - "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8", - "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615", - "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa", - "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4", - "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446", - "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4", - "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178", - "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af", - "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b", - "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe", - "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9", - "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09", - "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a", - "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063", - "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef", - "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1", - "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66", - "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231", - "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e", - "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec", - "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430", - "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce", - "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9", - "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa", - "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880" + "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", "markers": "python_version >= '3.7'", - "version": "==2.0.21" + "version": "==2.0.36" }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "tomlkit": { "hashes": [ - "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86", - "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899" - ], - "markers": "python_version >= '3.7'", - "version": "==0.12.1" - }, - "tornado": { - "hashes": [ - "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", - "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", - "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", - "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", - "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", - "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", - "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", - "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", - "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", - "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", - "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", - "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", - "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", - "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", - "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", - "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", - "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", - "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", - "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", - "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", - "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", - "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", - "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", - "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", - "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", - "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", - "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", - "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", - "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", - "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", - "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", - "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", - "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", - "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", - "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", - "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", - "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", - "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", - "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", - "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", - "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" + "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", + "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79" ], - "markers": "python_version >= '3.5'", - "version": "==6.1" + "markers": "python_version >= '3.8'", + "version": "==0.13.2" }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version < '3.11'", - "version": "==4.8.0" + "markers": "python_version >= '3.8'", + "version": "==4.12.2" }, "tzlocal": { "hashes": [ - "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803", - "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f" + "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", + "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e" ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" + "markers": "python_version >= '3.8'", + "version": "==5.2" }, "urllib3": { "hashes": [ - "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594", - "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.5" + "markers": "python_version >= '3.8'", + "version": "==2.2.3" }, "virtualenv": { "hashes": [ - "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b", - "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752" + "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2", + "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655" ], - "markers": "python_version >= '3.7'", - "version": "==20.24.5" + "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" + "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.11'", - "version": "==1.15.0" + "markers": "python_version >= '3.6'", + "version": "==1.16.0" + }, + "zipp": { + "hashes": [ + "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" + ], + "markers": "python_version >= '3.8'", + "version": "==3.20.2" } }, "develop": { "autopep8": { "hashes": [ - "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb", - "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c" + "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", + "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2.0.4" + "markers": "python_version >= '3.8'", + "version": "==2.3.1" }, "pycodestyle": { "hashes": [ - "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", - "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" + "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", + "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521" ], "markers": "python_version >= '3.8'", - "version": "==2.11.0" + "version": "==2.12.1" }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "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 index 3e8e562..387eb9c 100644 --- a/app.py +++ b/app.py @@ -1,58 +1,128 @@ from telegram.ext import ( - Updater, + ApplicationBuilder, CommandHandler, - Filters, + 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(): - updater = Updater(os.environ["TELEGRAM_TOKEN"]) - dp = updater.dispatcher + 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) - dp.add_handler(CommandHandler("help", handlers.help_handler)) + 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 - dp.add_handler( + application.add_handler( + ChatMemberHandler( + handlers.my_chat_member_handler, + ChatMemberHandler.MY_CHAT_MEMBER, + ) + ) + application.add_handler( MessageHandler( - Filters.entity("hashtag"), + filters.Entity("hashtag") & filters.ChatType.GROUPS, handlers.on_hashtag_message, - pass_job_queue=True, - pass_user_data=True, ) ) - dp.add_handler( + application.add_handler( MessageHandler( - Filters.status_update.new_chat_members & filter_bot_added, - handlers.on_new_chat_member, - pass_job_queue=True, + filters.StatusUpdate.NEW_CHAT_MEMBERS & filter_bot_added, + handlers.on_new_chat_members, ) ) # admin UX - dp.add_handler( - CommandHandler("start", handlers.start_handler, pass_user_data=True) - ) - dp.add_handler(CallbackQueryHandler(handlers.button_handler, pass_user_data=True)) - dp.add_handler( - MessageHandler( - (Filters.text | Filters.entity), - handlers.message_handler, - pass_user_data=True, - pass_job_queue=True, - ) + 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", ) - dp.add_error_handler(handlers.error_handler) + tg_logger.info(f"Scheduled metrics_exporter job: {job.name}") - updater.start_polling() tg_logger.info("Bot has started successfully") - updater.idle() + application.run_polling() if __name__ == "__main__": - main() \ No newline at end of file + 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 1a57ab5..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 src 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/src/constants.py b/src/constants.py index f64d996..649f8d7 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,37 +1,11 @@ from enum import IntEnum, auto +import json, os -# MESSAGES -on_set_new_message = "Обновил сообщение." -on_success_set_kick_timeout_response = "Обновил таймаут кика." -on_failed_set_kick_timeout_response = "Таймаут должен быть целым положительным числом" -on_failed_kick_response = "Я не справился." -on_success_kick_response = "%USER\_MENTION% не представился и был кикнут из чата." -on_start_command = "Выберите чат и действие:" -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} ---- -Сообщение после кика: {on_kick_message} -""" - -default_kick_timeout = 0 -notify_delta = 10 -min_whois_length = 60 + +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 @@ -45,16 +19,21 @@ class Actions(IntEnum): 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", "[]")) -RH_kick_messages = [ - "Хакер %USER\_MENTION% молчит и покидает чат. ⚰", - "Хакера %USER\_MENTION% забрал роскомнадзор", - "Хакера %USER\_MENTION% забрал Интерпол", - "Хакер %USER\_MENTION% провалил дедлайн", - "Хакер %USER\_MENTION% не смог выйти из VIM", - "Хакер %USER\_MENTION% пошёл кормить рыбок", - "Хакер %USER\_MENTION% провалил испытание", -] -RH_CHAT_ID = -1001147286684 +def get_uri(): + return os.environ.get( + "DATABASE_URL", "postgresql+asyncpg://user:password@wachter-db/db" + ) diff --git a/src/custom_filters.py b/src/custom_filters.py index 868f0ce..9ef68a3 100644 --- a/src/custom_filters.py +++ b/src/custom_filters.py @@ -1,7 +1,9 @@ -from telegram.ext import MessageFilter +from telegram.ext import filters -class FilterBotAdded(MessageFilter): # 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 index 86595f6..ed0441d 100644 --- a/src/handlers/__init__.py +++ b/src/handlers/__init__.py @@ -6,4 +6,5 @@ from .error_handler import error_handler from .admin import * +from .debug import * from .group import * diff --git a/src/handlers/admin/menu_handler.py b/src/handlers/admin/menu_handler.py index 38d7491..c27c55a 100644 --- a/src/handlers/admin/menu_handler.py +++ b/src/handlers/admin/menu_handler.py @@ -1,202 +1,436 @@ import json -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, Update -from telegram.ext import CallbackContext +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, User, session_scope +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 authorize_user +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 -def button_handler(update: Update, context: CallbackContext): +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: - with session_scope() as sess: - user_id = query.from_user.id - users = sess.query(User).filter(User.user_id == user_id) - user_chats = [ - { - "title": context.bot.get_chat(x.chat_id).title or x.chat_id, - "id": x.chat_id, - } - for x in users - ] - + user_id = query.from_user.id + user_chats = await get_chats_list(user_id, context) if len(user_chats) == 0: - update.message.reply_text("У вас нет доступных чатов.") + 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, + ) - keyboard = [ + if data["action"] == constants.Actions.select_chat: + selected_chat_id = data["chat_id"] + button_configs = [ + [{"text": _("btn__intro"), "action": constants.Actions.set_intro_settings}], [ - InlineKeyboardButton( - chat["title"], - callback_data=json.dumps( - {"chat_id": chat["id"], "action": constants.Actions.select_chat} - ), - ) - ] - for chat in user_chats - if authorize_user(context.bot, chat["id"], user_id) + { + "text": _("btn__kicks"), + "action": constants.Actions.set_kick_bans_settings, + } + ], + [ + { + "text": _("btn__back_to_chats"), + "action": constants.Actions.back_to_chats, + } + ], ] - - reply_markup = InlineKeyboardMarkup(keyboard) - context.bot.edit_message_reply_markup( + 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, ) - - if data["action"] == constants.Actions.select_chat: + elif data["action"] == constants.Actions.set_intro_settings: selected_chat_id = data["chat_id"] - keyboard = [ + button_configs = [ [ - InlineKeyboardButton( - "Изменить таймаут кика", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_kick_timeout, - } - ), - ) + { + "text": _("btn__current_settings"), + "action": constants.Actions.get_current_intro_settings, + } ], [ - InlineKeyboardButton( - "Изменить сообщение при входе в чат", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_on_new_chat_member_message_response, - } - ), - ) + { + "text": _("btn__change_welcome_message"), + "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, - } - ), - ) + { + "text": _("btn__change_rewelcome_message"), + "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, - } - ), - ) + { + "text": _("btn__change_notify_message"), + "action": constants.Actions.set_notify_message, + } ], [ - InlineKeyboardButton( - "Изменить сообщение напоминания", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_notify_message, - } - ), - ) + { + "text": _("btn__change_sucess_message"), + "action": constants.Actions.set_on_successful_introducion_response, + } ], [ - InlineKeyboardButton( - "Изменить сообщение после кика", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.set_on_kick_message, - } - ), - ) + { + "text": _("btn__change_notify_timeout"), + "action": constants.Actions.set_notify_timeout, + } ], [ - InlineKeyboardButton( - "Получить текущие настройки", - callback_data=json.dumps( - { - "chat_id": selected_chat_id, - "action": constants.Actions.get_current_settings, - } - ), - ) + { + "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, + ) - reply_markup = InlineKeyboardMarkup(keyboard) - context.bot.edit_message_reply_markup( + 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"] 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, - ]: - context.bot.edit_message_text( - text="Отправьте новое значение", + 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_settings: + elif data["action"] == constants.Actions.get_current_intro_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} - ), - ), - ], + 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) - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == data["chat_id"]).first() - context.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, - ) + 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 -def message_handler(update: Update, context: CallbackContext): - if not update.message: - update.message = update.edited_message +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. - chat_id = update.message.chat_id + Returns: + None: This function returns nothing. + """ + chat_id = update.effective_message.chat_id if chat_id > 0: action = context.user_data.get("action") @@ -207,64 +441,60 @@ def message_handler(update: Update, context: CallbackContext): chat_id = context.user_data["chat_id"] if action == constants.Actions.set_kick_timeout: - message = update.message.text + message = update.effective_message.text try: timeout = int(message) assert timeout >= 0 except: - update.message.reply_text(constants.on_failed_set_kick_timeout_response) + await update.effective_message.reply_text( + _("msg__failed_set_kick_timeout_response") + ) return - with session_scope() as sess: + async with session_scope() as sess: chat = Chat(id=chat_id, kick_timeout=timeout) - sess.merge(chat) + await sess.merge(chat) context.user_data["action"] = None + await _job_rescheduling_helper(on_kick_timeout, timeout, context, chat_id) - for job in context.job_queue.jobs(): - if job.name in [on_kick_timeout.__name__, on_notify_timeout.__name__]: - job_context = job.context - job_creation_time = datetime.fromtimestamp( - job_context.get("creation_time") + keyboard = [ + [ + new_button( + _("btn__back"), + chat_id, + constants.Actions.set_kick_bans_settings, ) - new_timeout = job_creation_time + timedelta(seconds=timeout * 60) - if job.name == on_kick_timeout.__name__: - if new_timeout < datetime.now(): - new_timeout = 0 - next_job_func = on_kick_timeout - else: - new_timeout = new_timeout - timedelta( - seconds=constants.notify_delta * 60 - ) - next_job_func = on_notify_timeout + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.effective_message.reply_text( + _("msg__success_set_kick_timeout_response"), + reply_markup=reply_markup, + ) - job.schedule_removal() - job_context["timeout"] = new_timeout - job = context.job_queue.run_once( - next_job_func, new_timeout, context=job_context - ) + 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 = [ [ - 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} - ), - ), - ], + new_button( + _("btn__back"), chat_id, constants.Actions.set_intro_settings + ) + ] ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - constants.on_success_set_kick_timeout_response, + await update.effective_message.reply_text( + _("msg__sucess_set_notify_timeout_response"), reply_markup=reply_markup, ) @@ -274,9 +504,12 @@ def message_handler(update: Update, context: CallbackContext): 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.message.text_markdown - with session_scope() as sess: + 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 ( @@ -290,31 +523,52 @@ def message_handler(update: Update, context: CallbackContext): 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) - sess.merge(chat) + 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 - context.user_data["action"] = None + 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) - 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} - ), - ), - ], - ] + 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) - update.message.reply_text( - constants.on_set_new_message, reply_markup=reply_markup - ) + 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 index 65153e5..f4c6abd 100644 --- a/src/handlers/admin/start_handler.py +++ b/src/handlers/admin/start_handler.py @@ -1,53 +1,41 @@ -import json -from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import CallbackContext +from telegram import InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes -from src import constants -from src.model import User, session_scope -from .utils import authorize_user +from src.handlers.utils import admin +from src.texts import _ -# todo @admin decorator to prevent / tweak behaviour when calling from group chats -# this will be a nice replacement for "if user_id < 0" checks -def start_handler(update: Update, context: CallbackContext): - user_id = update.message.chat_id +from .utils import get_chats_list, create_chats_list_keyboard - 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, context.bot)) +@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: - update.message.reply_text("У вас нет доступных чатов.") + await update.message.reply_text(_("msg__no_chats_available")) 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(context.bot, chat["id"], user_id) - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text(constants.on_start_command, reply_markup=reply_markup) - - -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 + # 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 index 317a621..0437826 100644 --- a/src/handlers/admin/utils.py +++ b/src/handlers/admin/utils.py @@ -1,9 +1,132 @@ -from telegram import Bot +import json +import time +from typing import Iterator, Dict, List +from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import CallbackContext -def authorize_user(bot: Bot, chat_id: int, user_id: int): +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: - status = bot.get_chat_member(chat_id, user_id).status - return status in ["creator", "administrator"] - except Exception: + 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 index c3439b4..db75461 100644 --- a/src/handlers/error_handler.py +++ b/src/handlers/error_handler.py @@ -1,10 +1,66 @@ -import logging - 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)" -def error_handler(update: Update, context: CallbackContext): - tg_logger.warning(f'Update "{update}" caused error "{context.error}"') + # 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 index 5b53d04..d2e3dba 100644 --- a/src/handlers/group/__init__.py +++ b/src/handlers/group/__init__.py @@ -2,4 +2,5 @@ Module with all the telegram handlers related to group chat flow. """ -from .group_handler import on_hashtag_message, on_new_chat_member +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 index d1cfeb0..9e9da5c 100644 --- a/src/handlers/group/group_handler.py +++ b/src/handlers/group/group_handler.py @@ -1,209 +1,390 @@ from datetime import datetime, timedelta -import logging -import random -from telegram import Bot, Message, ParseMode, Update -from telegram.ext import CallbackContext +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 - - -logger = logging.getLogger(__name__) - - -def on_new_chat_member(update: Update, context: CallbackContext): +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 - user_id = update.message.new_chat_members[-1].id - - for job in context.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() + 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 + ] - if user is not None: - update.message.reply_text(chat.on_known_new_chat_member_message) - return + 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() - message = chat.on_new_chat_member_message - timeout = chat.kick_timeout + 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 message == constants.skip_on_new_chat_member_message: - return + 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, + ) - message_markdown = _mention_markdown(context.bot, chat_id, user_id, message) - msg = update.message.reply_text(message_markdown, parse_mode=ParseMode.MARKDOWN) + 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 timeout != 0: - if timeout >= 10: + if notify_timeout != 0: job = context.job_queue.run_once( on_notify_timeout, - (timeout - constants.notify_delta) * 60, - context={ + notify_timeout * 60, + chat_id=chat_id, + user_id=user_id, + data={ "chat_id": chat_id, "user_id": user_id, - "job_queue": context.job_queue, "creation_time": datetime.now().timestamp(), }, ) - job = context.job_queue.run_once( - on_kick_timeout, - timeout * 60, - context={ - "chat_id": chat_id, - "user_id": user_id, - "message_id": msg.message_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 + ) -def on_hashtag_message(update: Update, context: CallbackContext): - if not update.message: - update.message = update.edited_message - chat_id = update.message.chat_id +async def remove_user_jobs_from_queue(context, user_id, chat_id): + """ + Remove jobs related to a specific user from the job queue. - 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 + 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 - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == chat_id).first() +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(id=chat_id) + chat = Chat.get_new_chat(chat_id) sess.add(chat) - sess.commit() + 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 - with session_scope() as sess: - user = User(chat_id=chat_id, user_id=user_id, whois=update.message.text) - sess.merge(user) + 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 - for job in context.job_queue.jobs(): - if ( - job.context["user_id"] == user_id - and job.context["chat_id"] == chat_id - and job.enabled == True - ): - try: - context.bot.delete_message( - job.context["chat_id"], job.context["message_id"] - ) - except: - pass - job.enabled = False - job.schedule_removal() - removed = True + removed = await remove_user_jobs_from_queue(context, user_id, chat_id) if removed: - message_markdown = _mention_markdown(context.bot, chat_id, user_id, message) - update.message.reply_text(message_markdown, parse_mode=ParseMode.MARKDOWN) + await _send_message_with_deletion( + context, + chat_id, + user_id, + message, + reply_to=update.effective_message, + ) -def on_notify_timeout(context: CallbackContext): - bot, job = context.bot, context.job - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == job.context["chat_id"]).first() +async def on_notify_timeout(context: ContextTypes.DEFAULT_TYPE): + """ + Send notify message, schedule its deletion. - message_markdown = _mention_markdown( - bot, job.context["chat_id"], job.context["user_id"], chat.notify_message - ) + Args: + context (CallbackContext): The context object containing the job details and bot instance. - message = bot.send_message( - job.context["chat_id"], text=message_markdown, parse_mode=ParseMode.MARKDOWN + 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"]) ) - - 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, - }, + 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, ) -def on_kick_timeout(context: CallbackContext): +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: - 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"], + 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) - with session_scope() as sess: - chat = sess.query(Chat).filter(Chat.id == job.context["chat_id"]).first() + 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"]: - message_markdown = _mention_markdown( - bot, - job.context["chat_id"], - job.context["user_id"], + await _send_message_with_deletion( + context, + job.data.get("chat_id"), + job.data.get("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: - logger.error(e) - bot.send_message(job.context["chat_id"], text=constants.on_failed_kick_response) + 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. -def _delete_message(context: CallbackContext): + Returns: + None + """ bot, job = context.bot, context.job try: - bot.delete_message(job.context["chat_id"], job.context["message_id"]) - except: - print(f"can't delete {job.context['message_id']} from {job.context['chat_id']}") + 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, + ) -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() +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() # \ нужен из-за формата сообщений в маркдауне - return message.replace("%USER\_MENTION%", 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 index 3e5ed26..f694683 100644 --- a/src/handlers/help_handler.py +++ b/src/handlers/help_handler.py @@ -1,7 +1,9 @@ from telegram import Update -from telegram.ext import CallbackContext +from telegram.constants import ParseMode +from telegram.ext import ContextTypes -from src import constants +from src.texts import _ -def help_handler(update: Update, _: CallbackContext): - update.message.reply_text(constants.help_message) + +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 index e5880f8..823eaa8 100644 --- a/src/logging.py +++ b/src/logging.py @@ -2,22 +2,55 @@ 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_logger.TelegramHandler", + "class": "telegram_handler.TelegramHandler", "token": os.environ["TELEGRAM_TOKEN"], - "chat_ids": [os.environ["TELEGRAM_ERROR_CHAT_ID"]], - } + "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",] + "handlers": [ + "wachter_telegram", + "wachter_oltp", + ], } - } + }, } config.dictConfig(log_config) diff --git a/src/model.py b/src/model.py index 5f290ef..0fd386a 100644 --- a/src/model.py +++ b/src/model.py @@ -1,9 +1,12 @@ 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 contextmanager -import os +from contextlib import asynccontextmanager + +from src import constants +from src.texts import _ Base = declarative_base() @@ -16,26 +19,62 @@ class Chat(Base): on_new_chat_member_message = Column( Text, nullable=False, - default="Пожалуйста, представьтесь и поздоровайтесь с сообществом.", ) on_known_new_chat_member_message = Column( - Text, nullable=False, default="Добро пожаловать. Снова" + Text, + nullable=False, + ) + on_introduce_message = Column( + Text, + nullable=False, ) - on_introduce_message = Column(Text, nullable=False, default="Добро пожаловать.") on_kick_message = Column( - Text, nullable=False, default="%USER\_MENTION% молчит и покидает чат" + Text, + nullable=False, ) notify_message = Column( Text, nullable=False, - default="%USER\_MENTION%, пожалуйста, представьтесь и поздоровайтесь с сообществом.", ) 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, default=0) + 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): @@ -47,25 +86,21 @@ class User(Base): 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) +engine = create_async_engine(constants.get_uri(), echo=False) +AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) -@contextmanager -def session_scope(): - session = Session() - try: - yield session - session.commit() - except: - session.rollback() - raise - finally: - session.close() +@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): 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" + ), + ), + ) + ), + )